protected static void prepareConnection(HttpURLConnection conn) throws ProtocolException {
    conn.setRequestMethod("GET");

    Settings s = Settings.getInstance();
    conn.setConnectTimeout(1000 * s.httpConnectionTimeout);
    conn.setReadTimeout(1000 * s.httpReadTimeout);
    if (conn.getRequestProperty("User-agent") == null)
      conn.setRequestProperty("User-agent", s.getUserAgent());
    conn.setRequestProperty("Accept", ACCEPT);
  }
 @Override
 public String getTileUrl(int zoom, int tilex, int tiley) {
   String ticket = Settings.getInstance().osmHikingTicket;
   if (ticket != null && ticket.length() > 0) {
     return ABO + super.getTileUrl(zoom, tilex, tiley) + "/ticket/" + ticket;
   } else return null;
 }
  public static void load() {
    File mapSourcesDir = Settings.getInstance().getMapSourcesDirectory();
    File mapSourcesProperties = new File(mapSourcesDir, FILENAME);
    if (!mapSourcesProperties.isFile()) return;
    FileInputStream in = null;
    try {
      in = new FileInputStream(mapSourcesProperties);
      PROPERTIES.load(in);
    } catch (IOException e) {
      log.error("Failed to load mapsources.properties", e);
    } finally {
      Utilities.closeStream(in);
    }
    if (!SHUTDOWN_HOOK_REGISTERED) {
      Runtime.getRuntime()
          .addShutdownHook(
              new Thread() {

                @Override
                public void run() {
                  save();
                }
              });
    }
  }
 /**
  * Reads all available data from the input stream of <code>conn</code> and returns it as byte
  * array. If no input data is available the method returns <code>null</code>.
  *
  * @param conn
  * @return
  * @throws IOException
  */
 protected static byte[] loadBodyDataInBuffer(HttpURLConnection conn) throws IOException {
   InputStream input = conn.getInputStream();
   byte[] data = null;
   try {
     if (Thread.currentThread() instanceof MapSourceListener) {
       // We only throttle atlas downloads, not downloads for the preview map
       long bandwidthLimit = Settings.getInstance().getBandwidthLimit();
       if (bandwidthLimit > 0) {
         input = new ThrottledInputStream(input);
       }
     }
     data = Utilities.getInputBytes(input);
   } catch (IOException e) {
     InputStream errorIn = conn.getErrorStream();
     try {
       byte[] errData = Utilities.getInputBytes(errorIn);
       log.trace(
           "Retrieved " + errData.length + " error bytes for a HTTP " + conn.getResponseCode());
     } catch (Exception ee) {
       log.debug("Error retrieving error stream content: " + e);
     } finally {
       Utilities.closeStream(errorIn);
     }
     throw e;
   } finally {
     Utilities.closeStream(input);
   }
   log.trace("Retrieved " + data.length + " bytes for a HTTP " + conn.getResponseCode());
   if (data.length == 0) return null;
   return data;
 }
  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;
  }
 public static void save() {
   if (PROPERTIES.size() == 0) return;
   File mapSourcesDir = Settings.getInstance().getMapSourcesDirectory();
   File mapSourcesProperties = new File(mapSourcesDir, FILENAME);
   FileOutputStream out = null;
   try {
     out = new FileOutputStream(mapSourcesProperties);
     PROPERTIES.store(out, "");
   } catch (IOException e) {
     log.error("", e);
   } finally {
     Utilities.closeStream(out);
   }
 }
Example #7
0
 public void actionPerformed(ActionEvent event) {
   Object source = event.getSource();
   File atlasFolder = Settings.getInstance().getAtlasOutputDirectory();
   if (openProgramFolderButton.equals(source)) {
     try {
       OSUtilities.openFolderBrowser(atlasFolder);
     } catch (Exception e) {
       log.error("", e);
     }
   } else if (dismissWindowButton.equals(source)) {
     downloadController = null;
     closeWindow();
   } else if (abortAtlasCreationButton.equals(source)) {
     aborted = true;
     stopUpdateTask();
     if (downloadController != null) downloadController.abortAtlasCreation();
     else closeWindow();
   } else if (pauseResumeDownloadButton.equals(source)) {
     if (downloadController != null) downloadController.pauseResumeAtlasCreation();
   }
 }
public class TileDownLoader {

  public static String ACCEPT = "text/html, image/png, image/jpeg, image/gif, */*;q=0.1";

  static {
    Object defaultReadTimeout = System.getProperty("sun.net.client.defaultReadTimeout");
    if (defaultReadTimeout == null)
      System.setProperty("sun.net.client.defaultReadTimeout", "15000");
    System.setProperty("http.maxConnections", "20");
  }

  private static Logger log = Logger.getLogger(TileDownLoader.class);

  private static Settings settings = Settings.getInstance();

  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;
  }

  private static void notifyTileDownloaded(int size) {
    if (Thread.currentThread() instanceof MapSourceListener) {
      ((MapSourceListener) Thread.currentThread()).tileDownloaded(size);
    }
  }

  private static void notifyCachedTileUsed(int size) {
    if (Thread.currentThread() instanceof MapSourceListener) {
      ((MapSourceListener) Thread.currentThread()).tileLoadedFromCache(size);
    }
  }

  /**
   * Download the tile from the web server and updates the tile store if the tile could be
   * successfully retrieved.
   *
   * @param x
   * @param y
   * @param zoom
   * @param mapSource
   * @return
   * @throws UnrecoverableDownloadException
   * @throws IOException
   * @throws InterruptedException
   */
  public static byte[] downloadTileAndUpdateStore(int x, int y, int zoom, HttpMapSource mapSource)
      throws UnrecoverableDownloadException, IOException, InterruptedException {
    return downloadTileAndUpdateStore(
        x, y, zoom, mapSource, Settings.getInstance().tileStoreEnabled);
  }

  public static byte[] downloadTile(int x, int y, int zoom, HttpMapSource mapSource)
      throws UnrecoverableDownloadException, IOException, InterruptedException {
    return downloadTileAndUpdateStore(x, y, zoom, mapSource, false);
  }

  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;
  }

  public static boolean isTileExpired(TileStoreEntry tileStoreEntry) {
    if (tileStoreEntry == null) return true;
    long expiredTime = tileStoreEntry.getTimeExpires();
    if (expiredTime >= 0) {
      // server had set an expiration time
      long maxExpirationTime = settings.tileMaxExpirationTime + tileStoreEntry.getTimeDownloaded();
      long minExpirationTime = settings.tileMinExpirationTime + tileStoreEntry.getTimeDownloaded();
      expiredTime = Math.max(minExpirationTime, Math.min(maxExpirationTime, expiredTime));
    } else {
      // no expiration time set by server - use the default one
      expiredTime = tileStoreEntry.getTimeDownloaded() + settings.tileDefaultExpirationTime;
    }
    return (expiredTime < System.currentTimeMillis());
  }

  /**
   * Reads all available data from the input stream of <code>conn</code> and returns it as byte
   * array. If no input data is available the method returns <code>null</code>.
   *
   * @param conn
   * @return
   * @throws IOException
   */
  protected static byte[] loadBodyDataInBuffer(HttpURLConnection conn) throws IOException {
    InputStream input = conn.getInputStream();
    byte[] data = null;
    try {
      if (Thread.currentThread() instanceof MapSourceListener) {
        // We only throttle atlas downloads, not downloads for the preview map
        long bandwidthLimit = Settings.getInstance().getBandwidthLimit();
        if (bandwidthLimit > 0) {
          input = new ThrottledInputStream(input);
        }
      }
      data = Utilities.getInputBytes(input);
    } catch (IOException e) {
      InputStream errorIn = conn.getErrorStream();
      try {
        byte[] errData = Utilities.getInputBytes(errorIn);
        log.trace(
            "Retrieved " + errData.length + " error bytes for a HTTP " + conn.getResponseCode());
      } catch (Exception ee) {
        log.debug("Error retrieving error stream content: " + e);
      } finally {
        Utilities.closeStream(errorIn);
      }
      throw e;
    } finally {
      Utilities.closeStream(input);
    }
    log.trace("Retrieved " + data.length + " bytes for a HTTP " + conn.getResponseCode());
    if (data.length == 0) return null;
    return data;
  }

  /**
   * Performs a <code>HEAD</code> request for retrieving the <code>LastModified</code> header value.
   */
  protected static boolean isTileNewer(TileStoreEntry tile, HttpMapSource mapSource)
      throws IOException {
    long oldLastModified = tile.getTimeLastModified();
    if (oldLastModified <= 0) {
      log.warn(
          "Tile age comparison not possible: "
              + "tile in tilestore does not contain lastModified attribute");
      return true;
    }
    HttpURLConnection conn =
        mapSource.getTileUrlConnection(tile.getZoom(), tile.getX(), tile.getY());
    conn.setRequestMethod("HEAD");
    conn.setRequestProperty("Accept", ACCEPT);
    long newLastModified = conn.getLastModified();
    if (newLastModified == 0) return true;
    return (newLastModified > oldLastModified);
  }

  protected static boolean hasTileETag(TileStoreEntry tile, HttpMapSource mapSource)
      throws IOException {
    String eTag = tile.geteTag();
    if (eTag == null || eTag.length() == 0) {
      log.warn("ETag check not possible: " + "tile in tilestore does not contain ETag attribute");
      return true;
    }
    HttpURLConnection conn =
        mapSource.getTileUrlConnection(tile.getZoom(), tile.getX(), tile.getY());
    conn.setRequestMethod("HEAD");
    conn.setRequestProperty("Accept", ACCEPT);
    String onlineETag = conn.getHeaderField("ETag");
    if (onlineETag == null || onlineETag.length() == 0) return true;
    return (onlineETag.equals(eTag));
  }

  protected static void prepareConnection(HttpURLConnection conn) throws ProtocolException {
    conn.setRequestMethod("GET");

    Settings s = Settings.getInstance();
    conn.setConnectTimeout(1000 * s.httpConnectionTimeout);
    conn.setReadTimeout(1000 * s.httpReadTimeout);
    if (conn.getRequestProperty("User-agent") == null)
      conn.setRequestProperty("User-agent", s.getUserAgent());
    conn.setRequestProperty("Accept", ACCEPT);
  }

  protected static void checkContentType(HttpURLConnection conn, byte[] data)
      throws UnrecoverableDownloadException {
    String contentType = conn.getContentType();
    if (contentType != null) {
      contentType = contentType.toLowerCase();
      if (!contentType.startsWith("image/")) {
        if (log.isTraceEnabled() && contentType.startsWith("text/")) {
          log.trace("Content (" + contentType + "): " + new String(data));
        }
        throw new UnrecoverableDownloadException(
            "Content type of the loaded image is unknown: " + contentType,
            UnrecoverableDownloadException.ERROR_CODE_CONTENT_TYPE);
      }
    }
  }

  /**
   * Check if the retrieved data length is equal to the header value Content-Length
   *
   * @param conn
   * @param data
   * @throws UnrecoverableDownloadException
   */
  protected static void checkContentLength(HttpURLConnection conn, byte[] data)
      throws UnrecoverableDownloadException {
    int len = conn.getContentLength();
    if (len < 0) return;
    if (data.length != len)
      throw new UnrecoverableDownloadException(
          "Content length is not as declared by the server: retrived="
              + data.length
              + " bytes  expected-content-length="
              + len
              + " bytes");
  }
}
  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;
  }
 /**
  * Download the tile from the web server and updates the tile store if the tile could be
  * successfully retrieved.
  *
  * @param x
  * @param y
  * @param zoom
  * @param mapSource
  * @return
  * @throws UnrecoverableDownloadException
  * @throws IOException
  * @throws InterruptedException
  */
 public static byte[] downloadTileAndUpdateStore(int x, int y, int zoom, HttpMapSource mapSource)
     throws UnrecoverableDownloadException, IOException, InterruptedException {
   return downloadTileAndUpdateStore(
       x, y, zoom, mapSource, Settings.getInstance().tileStoreEnabled);
 }
Example #11
0
  private void createComponents() {
    background = new JPanel(new GridBagLayout());

    windowTitle = new JLabel("<html><h3>ATLAS CREATION IN PROGRESS...</h3></html>");

    title = new JLabel("Processing maps of atlas:");

    mapInfoLabel =
        new JLabel(
            "Processing map ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn "
                + "of layer ABCDEFGHIJKLMNOPQRSTUVWXYZ from map source ABCDEFGHIJKLMNOPQRSTUVWXYZ");

    atlasMapsDone = new JLabel("000 of 000 done");
    atlasPercent = new JLabel(String.format(TEXT_TENTHPERCENT, 100.0));
    atlasTimeLeft = new JLabel("Time remaining: 00000 minutes 00 seconds", JLabel.RIGHT);
    atlasProgressBar = new JProgressBar();

    mapDownloadTitle = new JLabel(TEXT_MAP_DOWNLOAD + "000");
    mapDownloadElementsDone = new JLabel("1000000 of 1000000 tiles done");
    mapDownloadPercent = new JLabel(String.format(TEXT_PERCENT, 100));
    mapDownloadTimeLeft = new JLabel("Time remaining: 00000 minutes 00 seconds", JLabel.RIGHT);
    mapDownloadProgressBar = new JProgressBar();

    mapCreation = new JLabel("Map Creation");
    mapCreationProgressBar = new JProgressBar();

    nrOfDownloadedBytesPerSecond = new JLabel("Average download speed");
    nrOfDownloadedBytesPerSecondValue = new JLabel();
    nrOfDownloadedBytes = new JLabel("Downloaded");
    nrOfDownloadedBytesValue = new JLabel();
    nrOfCacheBytes = new JLabel("Loaded from tile store");
    nrOfCacheBytesValue = new JLabel();

    activeDownloads = new JLabel("Active tile fetcher threads");
    activeDownloadsValue = new JLabel();
    retryableDownloadErrors = new JLabel("Transient download errors");
    retryableDownloadErrors.setToolTipText(
        "<html><h4>Download errors for the current map and for the total atlas (transient/unrecoverable)</h4>"
            + "<p>Mobile Atlas Creator retries failed tile downloads up to two times. <br>"
            + "If the tile downloads fails the second time the tile will be counted as <br>"
            + "<b>unrecoverable</b> error and not tried again during the current map creation run.<br></p></html>");
    retryableDownloadErrorsValue = new JLabel();
    retryableDownloadErrorsValue.setToolTipText(retryableDownloadErrors.getToolTipText());
    permanentDownloadErrors = new JLabel("Unrecoverable download errors");
    permanentDownloadErrors.setToolTipText(retryableDownloadErrors.getToolTipText());
    permanentDownloadErrorsValue = new JLabel();
    permanentDownloadErrorsValue.setToolTipText(permanentDownloadErrors.getToolTipText());
    totalDownloadTime = new JLabel("Total creation time");
    totalDownloadTimeValue = new JLabel();

    ignoreDlErrors =
        new JCheckBox(
            "Ignore download errors and continue automatically",
            Settings.getInstance().ignoreDlErrors);
    ignoreDlErrors.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            downloadController.setIgnoreErrors(ignoreDlErrors.isSelected());
          }
        });
    statusLabel = new JLabel("Status:");
    Font f = statusLabel.getFont();
    statusLabel.setFont(f.deriveFont(Font.BOLD));
    abortAtlasCreationButton = new JButton("Abort creation");
    abortAtlasCreationButton.setToolTipText("Abort current Atlas download");
    dismissWindowButton = new JButton("Close Window");
    dismissWindowButton.setToolTipText("Atlas creation in progress...");
    dismissWindowButton.setVisible(false);
    openProgramFolderButton = new JButton("Open Atlas Folder");
    openProgramFolderButton.setToolTipText("Atlas creation in progress...");
    openProgramFolderButton.setEnabled(false);
    pauseResumeDownloadButton = new JButton("Pause/Resume");

    GBC gbcRIF = GBC.std().insets(0, 0, 20, 0).fill(GBC.HORIZONTAL);
    GBC gbcEol = GBC.eol();
    GBC gbcEolFill = GBC.eol().fill(GBC.HORIZONTAL);
    GBC gbcEolFillI = GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 0);

    // background.add(windowTitle, gbcEolFill);
    // background.add(Box.createVerticalStrut(10), gbcEol);

    background.add(mapInfoLabel, gbcEolFill);
    background.add(Box.createVerticalStrut(20), gbcEol);

    background.add(title, gbcRIF);
    background.add(atlasMapsDone, gbcRIF);
    background.add(atlasPercent, gbcRIF);
    background.add(atlasTimeLeft, gbcEolFill);
    background.add(atlasProgressBar, gbcEolFillI);
    background.add(Box.createVerticalStrut(20), gbcEol);

    background.add(mapDownloadTitle, gbcRIF);
    background.add(mapDownloadElementsDone, gbcRIF);
    background.add(mapDownloadPercent, gbcRIF);
    background.add(mapDownloadTimeLeft, gbcEolFill);
    background.add(mapDownloadProgressBar, gbcEolFillI);
    background.add(Box.createVerticalStrut(20), gbcEol);

    background.add(mapCreation, gbcEol);
    background.add(mapCreationProgressBar, gbcEolFillI);
    background.add(Box.createVerticalStrut(10), gbcEol);

    JPanel infoPanel = new JPanel(new GridBagLayout());
    GBC gbci = GBC.std().insets(0, 3, 3, 3);
    infoPanel.add(nrOfDownloadedBytes, gbci);
    infoPanel.add(nrOfDownloadedBytesValue, gbci.toggleEol());
    infoPanel.add(nrOfCacheBytes, gbci.toggleEol());
    infoPanel.add(nrOfCacheBytesValue, gbci.toggleEol());
    infoPanel.add(nrOfDownloadedBytesPerSecond, gbci.toggleEol());
    infoPanel.add(nrOfDownloadedBytesPerSecondValue, gbci.toggleEol());
    infoPanel.add(activeDownloads, gbci.toggleEol());
    infoPanel.add(activeDownloadsValue, gbci.toggleEol());
    infoPanel.add(retryableDownloadErrors, gbci.toggleEol());
    infoPanel.add(retryableDownloadErrorsValue, gbci.toggleEol());
    infoPanel.add(permanentDownloadErrors, gbci.toggleEol());
    infoPanel.add(permanentDownloadErrorsValue, gbci.toggleEol());
    infoPanel.add(totalDownloadTime, gbci.toggleEol());
    infoPanel.add(totalDownloadTimeValue, gbci.toggleEol());

    JPanel bottomPanel = new JPanel(new GridBagLayout());
    bottomPanel.add(infoPanel, GBC.std().gridheight(2).fillH());
    bottomPanel.add(ignoreDlErrors, GBC.eol().anchor(GBC.EAST));

    bottomPanel.add(statusLabel, GBC.eol().anchor(GBC.CENTER));

    GBC gbcRight = GBC.std().anchor(GBC.SOUTHEAST).insets(5, 0, 0, 0);
    bottomPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
    bottomPanel.add(abortAtlasCreationButton, gbcRight);
    bottomPanel.add(dismissWindowButton, gbcRight);
    bottomPanel.add(pauseResumeDownloadButton, gbcRight);
    bottomPanel.add(openProgramFolderButton, gbcRight);

    background.add(bottomPanel, gbcEolFillI);

    JPanel borderPanel = new JPanel(new GridBagLayout());
    borderPanel.add(background, GBC.std().insets(10, 10, 10, 10).fill());

    add(borderPanel, GBC.std().fill());

    abortAtlasCreationButton.addActionListener(this);
    dismissWindowButton.addActionListener(this);
    openProgramFolderButton.addActionListener(this);
    pauseResumeDownloadButton.addActionListener(this);
  }