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(xmlNearbyStationsParameters(location.id));

      return xmlNearbyStations(uri.toString());
    } else {
      throw new IllegalArgumentException("cannot handle: " + location.toDebugString());
    }
  }
  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 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());
    }
  }
  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();
  }