private static final void parseDateTime(final Calendar calendar, final CharSequence str) {
    final Matcher m = P_DATE_TIME.matcher(str);
    if (!m.matches()) throw new RuntimeException("cannot parse: '" + str + "'");

    ParserUtils.parseGermanDate(calendar, m.group(1));
    ParserUtils.parseEuropeanTime(calendar, m.group(2));
  }
  @Override
  public QueryConnectionsResult queryConnections(
      final Location from,
      final Location via,
      final Location to,
      final Date date,
      final boolean dep,
      final String products,
      final WalkSpeed walkSpeed)
      throws IOException {
    final String uri = connectionsQueryUri(from, via, to, date, dep, products);
    final CharSequence page = ParserUtils.scrape(uri);

    List<Location> fromAddresses = null;
    List<Location> viaAddresses = null;
    List<Location> 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<Location> addresses = new ArrayList<Location>();
      while (mAddresses.find()) {
        final String address = ParserUtils.resolveEntities(mAddresses.group(1)).trim();
        if (!addresses.contains(address))
          addresses.add(new Location(LocationType.ANY, 0, null, 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 IllegalStateException(type);
    }

    if (fromAddresses != null || viaAddresses != null || toAddresses != null)
      return new QueryConnectionsResult(
          new ResultHeader(SERVER_PRODUCT), fromAddresses, viaAddresses, toAddresses);
    else return queryConnections(uri, page);
  }
  public NearbyStationsResult queryNearbyStations(
      final Location location, final int maxDistance, final int maxStations) throws IOException {
    final StringBuilder uri = new StringBuilder(API_BASE);

    if (location.hasLocation()) {
      uri.append("query.exe/dny");
      uri.append("?performLocating=2&tpl=stop2json");
      uri.append("&look_maxno=").append(maxStations != 0 ? maxStations : 200);
      uri.append("&look_maxdist=").append(maxDistance != 0 ? maxDistance : 5000);
      uri.append("&look_stopclass=").append(allProductsInt());
      uri.append("&look_nv=get_stopweight|yes");
      uri.append("&look_x=").append(location.lon);
      uri.append("&look_y=").append(location.lat);

      return jsonNearbyStations(uri.toString());
    } else if (location.type == LocationType.STATION && location.hasId()) {
      uri.append("bhftafel.exe/dn");
      uri.append("?near=Anzeigen");
      uri.append("&distance=").append(maxDistance != 0 ? maxDistance / 1000 : 50);
      uri.append("&input=").append(location.id);

      final CharSequence page = ParserUtils.scrape(uri.toString());

      final Matcher m = P_NEARBY_STATIONS_BY_STATION.matcher(page);

      final List<Location> stations = new ArrayList<Location>();
      while (m.find()) {
        final int sId = Integer.parseInt(m.group(1));
        final String sName = ParserUtils.resolveEntities(m.group(2).trim());

        final Location station = new Location(LocationType.STATION, sId, null, sName);
        stations.add(station);
      }

      if (maxStations == 0 || maxStations >= stations.size())
        return new NearbyStationsResult(null, stations);
      else return new NearbyStationsResult(null, stations.subList(0, maxStations));
    } else {
      throw new IllegalArgumentException("cannot handle: " + location.toDebugString());
    }
  }
  public QueryDeparturesResult queryDepartures(
      final int stationId, final int maxDepartures, final boolean equivs) throws IOException {
    final ResultHeader header = new ResultHeader(SERVER_PRODUCT);
    final QueryDeparturesResult result = new QueryDeparturesResult(header);

    if (stationId < 1000000) // live
    {
      // scrape page
      final String uri = departuresQueryLiveUri(stationId);
      final CharSequence page = ParserUtils.scrape(uri);

      final Matcher mError = P_DEPARTURES_LIVE_ERRORS.matcher(page);
      if (mError.find()) {
        if (mError.group(1) != null)
          return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION);
        if (mError.group(2) != null)
          return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
        if (mError.group(3) != null) throw new UnexpectedRedirectException();
      }

      // parse page
      final Matcher mHead = P_DEPARTURES_LIVE_HEAD.matcher(page);
      if (mHead.matches()) {
        final String[] placeAndName =
            splitPlaceAndName(ParserUtils.resolveEntities(mHead.group(1)));
        final Calendar currentTime = new GregorianCalendar(timeZone());
        currentTime.clear();
        parseDateTime(currentTime, mHead.group(2));

        final Map<String, String> messages = new HashMap<String, String>();

        final Matcher mMsgsCoarse = P_DEPARTURES_LIVE_MSGS_COARSE.matcher(page);
        while (mMsgsCoarse.find()) {
          final Matcher mMsgsFine = P_DEPARTURES_LIVE_MSGS_FINE.matcher(mMsgsCoarse.group(1));
          if (mMsgsFine.matches()) {
            final String lineName = ParserUtils.resolveEntities(mMsgsFine.group(1));
            final char linePproduct = normalizeType(categoryFromName(lineName));
            final Line line = newLine(linePproduct, normalizeLineName(lineName), null);

            final String message =
                ParserUtils.resolveEntities(mMsgsFine.group(3)).replace('\n', ' ');
            messages.put(line.label, message);
          } else {
            throw new IllegalArgumentException(
                "cannot parse '" + mMsgsCoarse.group(1) + "' on " + uri);
          }
        }

        final List<Departure> departures = new ArrayList<Departure>(8);

        final Matcher mDepCoarse = P_DEPARTURES_LIVE_COARSE.matcher(page);
        while (mDepCoarse.find()) {
          final Matcher mDepFine = P_DEPARTURES_LIVE_FINE.matcher(mDepCoarse.group(1));
          if (mDepFine.matches()) {
            final Calendar parsedTime = new GregorianCalendar(timeZone());
            parsedTime.setTimeInMillis(currentTime.getTimeInMillis());
            ParserUtils.parseEuropeanTime(parsedTime, mDepFine.group(1));

            if (parsedTime.getTimeInMillis() - currentTime.getTimeInMillis()
                < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) parsedTime.add(Calendar.DAY_OF_MONTH, 1);

            boolean isPlanned = mDepFine.group(2) != null;

            Date plannedTime = null;
            Date predictedTime = null;
            if (!isPlanned) predictedTime = parsedTime.getTime();
            else plannedTime = parsedTime.getTime();

            final String lineName = ParserUtils.resolveEntities(mDepFine.group(3));
            final char lineProduct = normalizeType(categoryFromName(lineName));
            final Line line = newLine(lineProduct, normalizeLineName(lineName), null);

            final Position position = null;

            final String[] destinationPlaceAndName =
                splitPlaceAndName(ParserUtils.resolveEntities(mDepFine.group(4)));
            final Location destination =
                new Location(
                    LocationType.ANY, 0, destinationPlaceAndName[0], destinationPlaceAndName[1]);

            final String message = messages.get(line.label);

            final Departure dep =
                new Departure(
                    plannedTime, predictedTime, line, position, destination, null, message);
            if (!departures.contains(dep)) departures.add(dep);
          } else {
            throw new IllegalArgumentException(
                "cannot parse '" + mDepCoarse.group(1) + "' on " + uri);
          }
        }

        result.stationDepartures.add(
            new StationDepartures(
                new Location(LocationType.STATION, stationId, placeAndName[0], placeAndName[1]),
                departures,
                null));
        return result;
      } else {
        throw new IllegalArgumentException("cannot parse '" + page + "' on " + uri);
      }
    } else {
      // scrape page
      final String uri = departuresQueryPlanUri(stationId, maxDepartures);
      final CharSequence page = ParserUtils.scrape(uri);

      final Matcher mError = P_DEPARTURES_PLAN_ERRORS.matcher(page);
      if (mError.find()) {
        if (mError.group(1) != null)
          return new QueryDeparturesResult(header, QueryDeparturesResult.Status.INVALID_STATION);
        if (mError.group(2) != null)
          return new QueryDeparturesResult(header, QueryDeparturesResult.Status.SERVICE_DOWN);
        if (mError.group(3) != null) throw new UnexpectedRedirectException();
      }

      // parse page
      final Matcher mHead = P_DEPARTURES_PLAN_HEAD.matcher(page);
      if (mHead.matches()) {
        final String[] placeAndName =
            splitPlaceAndName(ParserUtils.resolveEntities(mHead.group(1)));
        final Calendar currentTime = new GregorianCalendar(timeZone());
        currentTime.clear();
        ParserUtils.parseGermanDate(currentTime, mHead.group(2));
        final List<Departure> departures = new ArrayList<Departure>(8);

        final Matcher mDepCoarse = P_DEPARTURES_PLAN_COARSE.matcher(page);
        while (mDepCoarse.find()) {
          final Matcher mDepFine = P_DEPARTURES_PLAN_FINE.matcher(mDepCoarse.group(1));
          if (mDepFine.matches()) {
            final Calendar parsedTime = new GregorianCalendar(timeZone());
            parsedTime.setTimeInMillis(currentTime.getTimeInMillis());
            ParserUtils.parseEuropeanTime(parsedTime, mDepFine.group(1));

            if (parsedTime.getTimeInMillis() - currentTime.getTimeInMillis()
                < -PARSER_DAY_ROLLOVER_THRESHOLD_MS) parsedTime.add(Calendar.DAY_OF_MONTH, 1);

            final Date plannedTime = parsedTime.getTime();

            final String lineName = ParserUtils.resolveEntities(mDepFine.group(2));
            final char lineProduct = normalizeType(categoryFromName(lineName));
            final Line line = newLine(lineProduct, normalizeLineName(lineName), null);

            final Position position = new Position(ParserUtils.resolveEntities(mDepFine.group(3)));

            final int destinationId = Integer.parseInt(mDepFine.group(4));
            final String[] destinationPlaceAndName =
                splitPlaceAndName(ParserUtils.resolveEntities(mDepFine.group(5)));
            final Location destination =
                new Location(
                    destinationId > 0 ? LocationType.STATION : LocationType.ANY,
                    destinationId,
                    destinationPlaceAndName[0],
                    destinationPlaceAndName[1]);

            final Departure dep =
                new Departure(plannedTime, null, line, position, destination, null, null);
            if (!departures.contains(dep)) departures.add(dep);
          } else {
            throw new IllegalArgumentException(
                "cannot parse '" + mDepCoarse.group(1) + "' on " + uri);
          }
        }

        result.stationDepartures.add(
            new StationDepartures(
                new Location(LocationType.STATION, stationId, placeAndName[0], placeAndName[1]),
                departures,
                null));
        return result;
      } else {
        throw new IllegalArgumentException("cannot parse '" + page + "' on " + uri);
      }
    }
  }
  public NearbyStationsResult queryNearbyStations(
      final Location location, final int maxDistance, final int maxStations) throws IOException {
    if (location.hasLocation()) {
      final StringBuilder uri = new StringBuilder(queryEndpoint);
      uri.append(jsonNearbyStationsParameters(location, maxDistance, maxStations));

      return jsonNearbyStations(uri.toString());
    } else if (location.type == LocationType.STATION && location.hasId()) {
      final StringBuilder uri = new StringBuilder(stationBoardEndpoint);
      uri.append("?near=Anzeigen");
      uri.append("&distance=").append(maxDistance != 0 ? maxDistance / 1000 : 50);
      uri.append("&input=").append(location.id);

      final CharSequence page = ParserUtils.scrape(uri.toString());

      final Matcher mError = P_NEARBY_ERRORS.matcher(page);
      if (mError.find()) {
        if (mError.group(1) != null)
          return new NearbyStationsResult(null, NearbyStationsResult.Status.INVALID_STATION);
      }

      final List<Location> stations = new ArrayList<Location>();

      final Matcher mOwn = P_NEARBY_OWN.matcher(page);
      if (mOwn.find()) {
        final int parsedId = Integer.parseInt(mOwn.group(1));
        final int parsedLon = (int) (Float.parseFloat(mOwn.group(2)) * 1E6);
        final int parsedLat = (int) (Float.parseFloat(mOwn.group(3)) * 1E6);
        final String[] parsedPlaceAndName =
            splitPlaceAndName(ParserUtils.urlDecode(mOwn.group(4), ISO_8859_1));
        stations.add(
            new Location(
                LocationType.STATION,
                parsedId,
                parsedLat,
                parsedLon,
                parsedPlaceAndName[0],
                parsedPlaceAndName[1]));
      }

      final Matcher mPage = P_NEARBY_PAGE.matcher(page);
      if (mPage.find()) {
        final Matcher mCoarse = P_NEARBY_COARSE.matcher(mPage.group(1));

        while (mCoarse.find()) {
          final Matcher mFineLocation = P_NEARBY_FINE_LOCATION.matcher(mCoarse.group(1));

          if (mFineLocation.find()) {
            final int parsedId = Integer.parseInt(mFineLocation.group(1));
            final String[] parsedPlaceAndName =
                splitPlaceAndName(ParserUtils.resolveEntities(mFineLocation.group(2)));
            final Location station =
                new Location(
                    LocationType.STATION, parsedId, parsedPlaceAndName[0], parsedPlaceAndName[1]);
            if (!stations.contains(station)) stations.add(station);
          } else {
            throw new IllegalArgumentException("cannot parse '" + mCoarse.group(1) + "' on " + uri);
          }
        }

        if (maxStations == 0 || maxStations >= stations.size())
          return new NearbyStationsResult(null, stations);
        else return new NearbyStationsResult(null, stations.subList(0, maxStations));
      } else {
        throw new IllegalArgumentException("cannot parse '" + page + "' on " + uri);
      }
    } else {
      throw new IllegalArgumentException("cannot handle: " + location.toDebugString());
    }
  }
  @Override
  public GetConnectionDetailsResult getConnectionDetails(final String uri) throws IOException {
    final CharSequence page = ParserUtils.scrape(uri);

    final Matcher mError = P_CONNECTION_DETAILS_ERROR.matcher(page);
    if (mError.find()) {
      if (mError.group(1) != null) throw new SessionExpiredException();
    }

    final Matcher mHead = P_CONNECTION_DETAILS_HEAD.matcher(page);
    if (mHead.matches()) {
      final List<Connection.Part> parts = new ArrayList<Connection.Part>(4);

      Date firstDepartureTime = null;
      Location firstDeparture = null;
      Date lastArrivalTime = null;
      Location lastArrival = null;

      final Matcher mDetCoarse = P_CONNECTION_DETAILS_COARSE.matcher(mHead.group(1));
      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 Location departure =
                new Location(
                    LocationType.ANY, 0, null, ParserUtils.resolveEntities(mDetFine.group(1)));
            if (departure != null && firstDeparture == null) firstDeparture = departure;

            if (mDetFine.group(2) != null) {
              final String lineStr = normalizeLine(ParserUtils.resolveEntities(mDetFine.group(2)));
              final Line line = new Line(null, lineStr, lineColors(lineStr));

              final Calendar departureTime = new GregorianCalendar(timeZone());
              departureTime.clear();
              ParserUtils.parseEuropeanTime(departureTime, mDetFine.group(3));
              ParserUtils.parseGermanDate(departureTime, mDetFine.group(5));

              final String departurePosition = ParserUtils.resolveEntities(mDetFine.group(4));

              final Location arrival =
                  new Location(
                      LocationType.ANY, 0, null, ParserUtils.resolveEntities(mDetFine.group(6)));

              final Calendar arrivalTime = new GregorianCalendar(timeZone());
              arrivalTime.clear();
              ParserUtils.parseEuropeanTime(arrivalTime, mDetFine.group(7));
              ParserUtils.parseGermanDate(arrivalTime, mDetFine.group(9));

              final String arrivalPosition = ParserUtils.resolveEntities(mDetFine.group(8));

              parts.add(
                  new Connection.Trip(
                      line,
                      null,
                      departureTime.getTime(),
                      departurePosition,
                      departure,
                      arrivalTime.getTime(),
                      arrivalPosition,
                      arrival,
                      null,
                      null));

              if (firstDepartureTime == null) firstDepartureTime = departureTime.getTime();

              lastArrival = arrival;
              lastArrivalTime = arrivalTime.getTime();
            } else if (mDetFine.group(10) != null) {
              final String min = mDetFine.group(10);

              final Location arrival =
                  new Location(
                      LocationType.ANY, 0, null, 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,
                        null));
              } else {
                parts.add(new Connection.Footway(Integer.parseInt(min), departure, arrival, null));
              }

              lastArrival = arrival;
            } else {
              final Location arrival =
                  new Location(
                      LocationType.ANY, 0, null, ParserUtils.resolveEntities(mDetFine.group(12)));

              parts.add(new Connection.Footway(0, departure, arrival, null));
            }
          } 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" + mHead.group(1) + "\n" + parts);

      return new GetConnectionDetailsResult(
          new GregorianCalendar(timeZone()).getTime(),
          new Connection(
              AbstractHafasProvider.extractConnectionId(uri),
              uri,
              firstDepartureTime,
              lastArrivalTime,
              firstDeparture,
              lastArrival,
              parts,
              null,
              null));
    } else {
      throw new IOException(page.toString());
    }
  }
  private QueryConnectionsResult queryConnections(final String uri, final CharSequence page)
      throws IOException {
    final Matcher mError = P_CHECK_CONNECTIONS_ERROR.matcher(page);
    if (mError.find()) {
      if (mError.group(1) != null)
        return new QueryConnectionsResult(null, QueryConnectionsResult.Status.TOO_CLOSE);
      if (mError.group(2) != null)
        return new QueryConnectionsResult(null, QueryConnectionsResult.Status.UNRESOLVABLE_ADDRESS);
      if (mError.group(3) != null)
        return new QueryConnectionsResult(null, QueryConnectionsResult.Status.NO_CONNECTIONS);
      if (mError.group(4) != null)
        return new QueryConnectionsResult(null, QueryConnectionsResult.Status.INVALID_DATE);
      if (mError.group(5) != null) throw new SessionExpiredException();
    }

    final Matcher mHead = P_CONNECTIONS_HEAD.matcher(page);
    if (mHead.matches()) {
      final Location from =
          new Location(LocationType.ANY, 0, null, ParserUtils.resolveEntities(mHead.group(1)));
      final Location to =
          new Location(LocationType.ANY, 0, null, ParserUtils.resolveEntities(mHead.group(2)));
      final Calendar currentDate = new GregorianCalendar(timeZone());
      currentDate.clear();
      ParserUtils.parseGermanDate(currentDate, 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<Connection> connections = new ArrayList<Connection>();

      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));
          final Calendar departureTime = new GregorianCalendar(timeZone());
          departureTime.setTimeInMillis(currentDate.getTimeInMillis());
          ParserUtils.parseEuropeanTime(departureTime, mConFine.group(2));
          if (!connections.isEmpty()) {
            final long diff =
                departureTime.getTimeInMillis()
                    - connections.get(connections.size() - 1).departureTime.getTime();
            if (diff > PARSER_DAY_ROLLOVER_THRESHOLD_MS)
              departureTime.add(Calendar.DAY_OF_YEAR, -1);
            else if (diff < -PARSER_DAY_ROLLOVER_THRESHOLD_MS)
              departureTime.add(Calendar.DAY_OF_YEAR, 1);
          }
          final Calendar arrivalTime = new GregorianCalendar(timeZone());
          arrivalTime.setTimeInMillis(currentDate.getTimeInMillis());
          ParserUtils.parseEuropeanTime(arrivalTime, mConFine.group(3));
          if (departureTime.after(arrivalTime)) arrivalTime.add(Calendar.DAY_OF_YEAR, 1);
          final Connection connection =
              new Connection(
                  AbstractHafasProvider.extractConnectionId(link),
                  link,
                  departureTime.getTime(),
                  arrivalTime.getTime(),
                  from,
                  to,
                  null,
                  null,
                  null);
          connections.add(connection);
        } else {
          throw new IllegalArgumentException(
              "cannot parse '" + mConCoarse.group(1) + "' on " + uri);
        }
      }

      return new QueryConnectionsResult(
          new ResultHeader(SERVER_PRODUCT), uri, from, null, to, linkLater, connections);
    } else {
      throw new IOException(page.toString());
    }
  }
 @Override
 public QueryConnectionsResult queryMoreConnections(final String uri) throws IOException {
   final CharSequence page = ParserUtils.scrape(uri);
   return queryConnections(uri, page);
 }
  private String connectionsQueryUri(
      final Location from,
      final Location via,
      final Location to,
      final Date date,
      final boolean dep,
      final String products) {
    final Calendar c = new GregorianCalendar(timeZone());
    c.setTime(date);

    final StringBuilder uri = new StringBuilder();

    uri.append(API_BASE).append("query.exe/dox");
    uri.append("?REQ0HafasOptimize1=0:1");

    uri.append("&REQ0JourneyStopsS0ID=").append(ParserUtils.urlEncode(locationId(from)));
    uri.append("&REQ0JourneyStopsZ0ID=").append(ParserUtils.urlEncode(locationId(to)));

    if (via != null) {
      // workaround, for there does not seem to be a REQ0JourneyStops1.0ID parameter

      uri.append("&REQ0JourneyStops1.0A=").append(locationType(via));

      if (via.type == LocationType.STATION && via.hasId() && isValidStationId(via.id)) {
        uri.append("&REQ0JourneyStops1.0L=").append(via.id);
      } else if (via.hasLocation()) {
        uri.append("&REQ0JourneyStops1.0X=").append(via.lon);
        uri.append("&REQ0JourneyStops1.0Y=").append(via.lat);
        if (via.name == null)
          uri.append("&REQ0JourneyStops1.0O=")
              .append(
                  ParserUtils.urlEncode(
                      String.format(Locale.ENGLISH, "%.6f, %.6f", via.lat / 1E6, via.lon / 1E6)));
      } else if (via.name != null) {
        uri.append("&REQ0JourneyStops1.0G=").append(ParserUtils.urlEncode(via.name));
        if (via.type != LocationType.ANY) uri.append('!');
      }
    }

    uri.append("&REQ0HafasSearchForw=").append(dep ? "1" : "0");
    uri.append("&REQ0JourneyDate=")
        .append(
            String.format(
                "%02d.%02d.%02d",
                c.get(Calendar.DAY_OF_MONTH),
                c.get(Calendar.MONTH) + 1,
                c.get(Calendar.YEAR) - 2000));
    uri.append("&REQ0JourneyTime=")
        .append(String.format("%02d:%02d", c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)));
    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");

    if (products != null) {
      for (final char p : products.toCharArray()) {
        if (p == 'I') {
          uri.append(
              "&REQ0JourneyProduct_prod_section_0_0=1&REQ0JourneyProduct_prod_section_0_1=1");
          if (via != null)
            uri.append(
                "&REQ0JourneyProduct_prod_section_1_0=1&REQ0JourneyProduct_prod_section_1_1=1");
        }
        if (p == 'R') {
          uri.append(
              "&REQ0JourneyProduct_prod_section_0_2=1&REQ0JourneyProduct_prod_section_0_3=1");
          if (via != null)
            uri.append(
                "&REQ0JourneyProduct_prod_section_1_2=1&REQ0JourneyProduct_prod_section_1_3=1");
        }
        if (p == 'S') {
          uri.append("&REQ0JourneyProduct_prod_section_0_4=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_4=1");
        }
        if (p == 'U') {
          uri.append("&REQ0JourneyProduct_prod_section_0_7=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_7=1");
        }
        if (p == 'T') {
          uri.append("&REQ0JourneyProduct_prod_section_0_8=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_8=1");
        }
        if (p == 'B') {
          uri.append("&REQ0JourneyProduct_prod_section_0_5=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_5=1");
        }
        if (p == 'P') {
          uri.append("&REQ0JourneyProduct_prod_section_0_9=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_9=1");
        }
        if (p == 'F') {
          uri.append("&REQ0JourneyProduct_prod_section_0_6=1");
          if (via != null) uri.append("&REQ0JourneyProduct_prod_section_1_6=1");
        }
        // FIXME if (p == 'C')
      }
    }

    return uri.toString();
  }
  public List<Location> autocompleteStations(final CharSequence constraint) throws IOException {
    final String uri =
        String.format(AUTOCOMPLETE_URI, ParserUtils.urlEncode(constraint.toString(), ENCODING));

    return jsonGetStops(uri);
  }