private void prepareTileStoreInfoPanel() {

    final GridBagConstraints gbc_mapSource = new GridBagConstraints();
    gbc_mapSource.insets = new Insets(5, 10, 5, 10);
    gbc_mapSource.anchor = GridBagConstraints.WEST;
    final GridBagConstraints gbc_mapTiles = new GridBagConstraints();
    gbc_mapTiles.insets = gbc_mapSource.insets;
    gbc_mapTiles.anchor = GridBagConstraints.EAST;
    final GridBagConstraints gbc_eol = new GridBagConstraints();
    gbc_eol.gridwidth = GridBagConstraints.REMAINDER;

    TileStore tileStore = TileStore.getInstance();
    MapSourcesManager mapSourcesManager = MapSourcesManager.getInstance();

    tileStoreInfoPanel.add(new JLabel("<html><b>Map source</b></html>"), gbc_mapSource);
    tileStoreInfoPanel.add(new JLabel("<html><b>Tiles</b></html>"), gbc_mapTiles);
    tileStoreInfoPanel.add(new JLabel("<html><b>Size</b></html>"), gbc_eol);

    ImageIcon trash = Utilities.loadResourceImageIcon("trash.png");

    for (String name : tileStore.getAllStoreNames()) {
      String mapTileCountText = "  ?  ";
      String mapTileSizeText = "    ?    ";
      MapSource mapSource = mapSourcesManager.getSourceByName(name);
      final JLabel mapSourceNameLabel;
      if (mapSource != null) mapSourceNameLabel = new JLabel(name);
      else mapSourceNameLabel = new JLabel(name + " (unused)");
      final JLabel mapTileCountLabel = new JLabel(mapTileCountText);
      final JLabel mapTileSizeLabel = new JLabel(mapTileSizeText);
      final JButton deleteButton = new JButton(trash);
      TileSourceInfoComponents info = new TileSourceInfoComponents();
      info.name = name;
      info.countLabel = mapTileCountLabel;
      info.sizeLabel = mapTileSizeLabel;
      tileStoreInfoList.add(info);
      deleteButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
      deleteButton.setToolTipText("Delete all stored " + name + " tiles.");
      deleteButton.addActionListener(new ClearTileCacheAction(name));

      tileStoreInfoPanel.add(mapSourceNameLabel, gbc_mapSource);
      tileStoreInfoPanel.add(mapTileCountLabel, gbc_mapTiles);
      tileStoreInfoPanel.add(mapTileSizeLabel, gbc_mapTiles);
      tileStoreInfoPanel.add(deleteButton, gbc_eol);
    }
    JSeparator hr = new JSeparator(JSeparator.HORIZONTAL);
    hr.setBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED));
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.fill = GridBagConstraints.HORIZONTAL;
    tileStoreInfoPanel.add(hr, gbc);

    JLabel totalMapLabel = new JLabel("<html><b>Total</b></html>");
    totalTileCountLabel = new JLabel("<html><b>??</b></html>");
    totalTileSizeLabel = new JLabel("<html><b>??</b></html>");
    tileStoreInfoPanel.add(totalMapLabel, gbc_mapSource);
    tileStoreInfoPanel.add(totalTileCountLabel, gbc_mapTiles);
    tileStoreInfoPanel.add(totalTileSizeLabel, gbc_mapTiles);
  }
  public static byte[] getImage(int x, int y, int zoom, HttpMapSource mapSource)
      throws IOException, InterruptedException, UnrecoverableDownloadException {

    MapSpace mapSpace = mapSource.getMapSpace();
    int maxTileIndex = mapSpace.getMaxPixels(zoom) / mapSpace.getTileSize();
    if (x > maxTileIndex)
      throw new RuntimeException("Invalid tile index x=" + x + " for zoom " + zoom);
    if (y > maxTileIndex)
      throw new RuntimeException("Invalid tile index y=" + y + " for zoom " + zoom);

    TileStore ts = TileStore.getInstance();

    // Thread.sleep(2000);

    // Test code for creating random download failures
    // if (Math.random()>0.7) throw new
    // IOException("intentionally download error");

    Settings s = Settings.getInstance();

    TileStoreEntry tile = null;
    if (s.tileStoreEnabled) {

      // Copy the file from the persistent tilestore instead of
      // downloading it from internet.
      tile = ts.getTile(x, y, zoom, mapSource);
      boolean expired = isTileExpired(tile);
      if (tile != null) {
        if (expired) {
          log.trace("Expired: " + mapSource.getName() + " " + tile);
        } else {
          log.trace("Tile of map source " + mapSource.getName() + " used from tilestore");
          byte[] data = tile.getData();
          notifyCachedTileUsed(data.length);
          return data;
        }
      }
    }
    byte[] data = null;
    if (tile == null) {
      data = downloadTileAndUpdateStore(x, y, zoom, mapSource);
      notifyTileDownloaded(data.length);
    } else {
      byte[] updatedData = updateStoredTile(tile, mapSource);
      if (updatedData != null) {
        data = updatedData;
        notifyTileDownloaded(data.length);
      } else {
        data = tile.getData();
        notifyCachedTileUsed(data.length);
      }
    }
    return data;
  }
  /**
   * @param updateStoreName name of the tile store to update or <code>null</code> in case of all
   *     tile stores to be updated
   */
  private void updateTileStoreInfoPanel(String updateStoreName) {
    try {
      TileStore tileStore = TileStore.getInstance();

      long totalTileCount = 0;
      long totalTileSize = 0;
      for (final TileSourceInfoComponents info : tileStoreInfoList) {
        String storeName = info.name;
        Utilities.checkForInterruption();
        int count;
        long size;
        if (updateStoreName == null || info.name.equals(updateStoreName)) {
          TileStoreInfo tsi = tileStore.getStoreInfo(storeName);
          count = tsi.getTileCount();
          size = tsi.getStoreSize();
          info.count = count;
          info.size = size;
          final String mapTileCountText = (count < 0) ? "??" : Integer.toString(count);
          final String mapTileSizeText = Utilities.formatBytes(size);
          SwingUtilities.invokeLater(
              new Runnable() {
                public void run() {
                  info.countLabel.setText("<html><b>" + mapTileCountText + "</b></html>");
                  info.sizeLabel.setText("<html><b>" + mapTileSizeText + "</b></html>");
                }
              });
        } else {
          count = info.count;
          size = info.size;
        }
        totalTileCount += count;
        totalTileSize += size;
      }
      final String totalTileCountText = "<html><b>" + Long.toString(totalTileCount) + "</b></html>";
      final String totalTileSizeText =
          "<html><b>" + Utilities.formatBytes(totalTileSize) + "</b></html>";
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              totalTileCountLabel.setText(totalTileCountText);
              totalTileSizeLabel.setText(totalTileSizeText);
            }
          });
    } catch (InterruptedException e) {
      SettingsGUI.log.debug("Tile store information retrieval was canceled");
    }
  }
  public static byte[] downloadTileAndUpdateStore(
      int x, int y, int zoom, HttpMapSource mapSource, boolean useTileStore)
      throws UnrecoverableDownloadException, IOException, InterruptedException {

    if (zoom < 0) throw new UnrecoverableDownloadException("Negative zoom!");
    HttpURLConnection conn = mapSource.getTileUrlConnection(zoom, x, y);
    if (conn == null)
      throw new UnrecoverableDownloadException(
          "Tile x="
              + x
              + " y="
              + y
              + " zoom="
              + zoom
              + " is not a valid tile in map source "
              + mapSource);

    log.trace("Downloading " + conn.getURL());

    prepareConnection(conn);
    conn.connect();

    int code = conn.getResponseCode();
    byte[] data = loadBodyDataInBuffer(conn);

    if (code != HttpURLConnection.HTTP_OK) throw new DownloadFailedException(conn, code);

    checkContentType(conn, data);
    checkContentLength(conn, data);

    String eTag = conn.getHeaderField("ETag");
    long timeLastModified = conn.getLastModified();
    long timeExpires = conn.getExpiration();

    Utilities.checkForInterruption();
    TileImageType imageType = Utilities.getImageType(data);
    if (imageType == null)
      throw new UnrecoverableDownloadException("The returned image is of unknown format");
    if (useTileStore) {
      TileStore.getInstance()
          .putTileData(data, x, y, zoom, mapSource, timeLastModified, timeExpires, eTag);
    }
    Utilities.checkForInterruption();
    return data;
  }
  public static byte[] updateStoredTile(TileStoreEntry tile, HttpMapSource mapSource)
      throws UnrecoverableDownloadException, IOException, InterruptedException {
    final int x = tile.getX();
    final int y = tile.getY();
    final int zoom = tile.getZoom();
    final HttpMapSource.TileUpdate tileUpdate = mapSource.getTileUpdate();

    switch (tileUpdate) {
      case ETag:
        {
          boolean unchanged = hasTileETag(tile, mapSource);
          if (unchanged) {
            if (log.isTraceEnabled())
              log.trace("Data unchanged on server (eTag): " + mapSource + " " + tile);
            return null;
          }
          break;
        }
      case LastModified:
        {
          boolean isNewer = isTileNewer(tile, mapSource);
          if (!isNewer) {
            if (log.isTraceEnabled())
              log.trace("Data unchanged on server (LastModified): " + mapSource + " " + tile);
            return null;
          }
          break;
        }
    }
    HttpURLConnection conn = mapSource.getTileUrlConnection(zoom, x, y);
    if (conn == null)
      throw new UnrecoverableDownloadException(
          "Tile x="
              + x
              + " y="
              + y
              + " zoom="
              + zoom
              + " is not a valid tile in map source "
              + mapSource);

    if (log.isTraceEnabled()) log.trace(String.format("Checking %s %s", mapSource.getName(), tile));

    prepareConnection(conn);

    boolean conditionalRequest = false;

    switch (tileUpdate) {
      case IfNoneMatch:
        {
          if (tile.geteTag() != null) {
            conn.setRequestProperty("If-None-Match", tile.geteTag());
            conditionalRequest = true;
          }
          break;
        }
      case IfModifiedSince:
        {
          if (tile.getTimeLastModified() > 0) {
            conn.setIfModifiedSince(tile.getTimeLastModified());
            conditionalRequest = true;
          }
          break;
        }
    }

    conn.connect();

    Settings s = Settings.getInstance();

    int code = conn.getResponseCode();

    if (conditionalRequest && code == HttpURLConnection.HTTP_NOT_MODIFIED) {
      // Data unchanged on server
      if (s.tileStoreEnabled) {
        tile.update(conn.getExpiration());
        TileStore.getInstance().putTile(tile, mapSource);
      }
      if (log.isTraceEnabled()) log.trace("Data unchanged on server: " + mapSource + " " + tile);
      return null;
    }
    byte[] data = loadBodyDataInBuffer(conn);

    if (code != HttpURLConnection.HTTP_OK) throw new DownloadFailedException(conn, code);

    checkContentType(conn, data);
    checkContentLength(conn, data);

    String eTag = conn.getHeaderField("ETag");
    long timeLastModified = conn.getLastModified();
    long timeExpires = conn.getExpiration();

    Utilities.checkForInterruption();
    TileImageType imageType = Utilities.getImageType(data);
    if (imageType == null)
      throw new UnrecoverableDownloadException("The returned image is of unknown format");
    if (s.tileStoreEnabled) {
      TileStore.getInstance()
          .putTileData(data, x, y, zoom, mapSource, timeLastModified, timeExpires, eTag);
    }
    Utilities.checkForInterruption();
    return data;
  }