/******************************************************************************* * Copyright (c) MOBAC developers * * 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 2 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 <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.program; import java.awt.Toolkit; import java.io.File; import java.io.IOException; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import mobac.exceptions.AtlasTestException; import mobac.exceptions.MapDownloadSkippedException; import mobac.gui.AtlasProgress; import mobac.gui.AtlasProgress.AtlasCreationController; import mobac.program.atlascreators.AtlasCreator; import mobac.program.atlascreators.tileprovider.DownloadedTileProvider; import mobac.program.atlascreators.tileprovider.FilteredMapSourceProvider; import mobac.program.atlascreators.tileprovider.TileProvider; import mobac.program.download.DownloadJobProducerThread; import mobac.program.interfaces.AtlasInterface; import mobac.program.interfaces.DownloadJobListener; import mobac.program.interfaces.DownloadableElement; import mobac.program.interfaces.FileBasedMapSource; import mobac.program.interfaces.LayerInterface; import mobac.program.interfaces.MapInterface; import mobac.program.interfaces.MapSource; import mobac.program.interfaces.MapSource.LoadMethod; import mobac.program.model.AtlasOutputFormat; import mobac.program.model.Settings; import mobac.program.tilestore.TileStore; import mobac.utilities.GUIExceptionHandler; import mobac.utilities.I18nUtils; import mobac.utilities.Utilities; import mobac.utilities.tar.TarIndex; import mobac.utilities.tar.TarIndexedArchive; import org.apache.log4j.Logger; public class AtlasThread extends Thread implements DownloadJobListener, AtlasCreationController { private static final Logger log = Logger.getLogger(AtlasThread.class); private static int threadNum = 0; private File customAtlasDir = null; private boolean quitMobacAfterAtlasCreation = false; private DownloadJobProducerThread djp = null; private JobDispatcher downloadJobDispatcher; private AtlasProgress ap; // The GUI showing the progress private AtlasInterface atlas; private AtlasCreator atlasCreator = null; private PauseResumeHandler pauseResumeHandler; private int activeDownloads = 0; private int jobsCompleted = 0; private int jobsRetryError = 0; private int jobsPermanentError = 0; private int maxDownloadRetries = 1; public AtlasThread(AtlasInterface atlas) throws AtlasTestException { this(atlas, atlas.getOutputFormat().createAtlasCreatorInstance()); } public AtlasThread(AtlasInterface atlas, AtlasCreator atlasCreator) throws AtlasTestException { super("AtlasThread " + getNextThreadNum()); ap = new AtlasProgress(this); this.atlas = atlas; this.atlasCreator = atlasCreator; testAtlas(); TileStore.getInstance().closeAll(); maxDownloadRetries = Settings.getInstance().downloadRetryCount; pauseResumeHandler = new PauseResumeHandler(); } private void testAtlas() throws AtlasTestException { try { for (LayerInterface layer : atlas) { for (MapInterface map : layer) { MapSource mapSource = map.getMapSource(); if (!atlasCreator.testMapSource(mapSource)) throw new AtlasTestException("The selected atlas output format \"" + atlas.getOutputFormat() + "\" does not support the map source \"" + map.getMapSource() + "\""); } } } catch (AtlasTestException e) { throw e; } catch (Exception e) { throw new AtlasTestException(e); } } private static synchronized int getNextThreadNum() { threadNum++; return threadNum; } public void run() { GUIExceptionHandler.registerForCurrentThread(); log.info("Starting creation of " + atlas.getOutputFormat() + " atlas \"" + atlas.getName() + "\""); if (customAtlasDir != null) log.debug("Target directory: " + customAtlasDir); ap.setDownloadControlerListener(this); try { createAtlas(); log.info("Altas creation finished"); if (quitMobacAfterAtlasCreation) System.exit(0); } catch (OutOfMemoryError e) { System.gc(); SwingUtilities.invokeLater(new Runnable() { public void run() { String message = I18nUtils.localizedStringForKey("msg_out_of_memory_head"); int maxMem = Utilities.getJavaMaxHeapMB(); if (maxMem > 0) message += String.format(I18nUtils.localizedStringForKey("msg_out_of_memory_detail"), maxMem); JOptionPane.showMessageDialog(null, message, I18nUtils.localizedStringForKey("msg_out_of_memory_title"), JOptionPane.ERROR_MESSAGE); ap.closeWindow(); } }); log.error("Out of memory: ", e); } catch (InterruptedException e) { SwingUtilities.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("msg_atlas_download_abort"), I18nUtils.localizedStringForKey("Information"), JOptionPane.INFORMATION_MESSAGE); ap.closeWindow(); } }); log.info("Altas creation was interrupted by user"); } catch (Exception e) { log.error("Altas creation aborted because of an error: ", e); GUIExceptionHandler.showExceptionDialog(e); } System.gc(); if (quitMobacAfterAtlasCreation) { try { Thread.sleep(5000); } catch (InterruptedException e) { } System.exit(1); } } /** * Create atlas: For each map download the tiles and perform atlas/map creation */ protected void createAtlas() throws InterruptedException, IOException { long totalNrOfOnlineTiles = atlas.calculateTilesToDownload(); for (LayerInterface l : atlas) { for (MapInterface m : l) { // Offline map sources are not relevant for the maximum tile limit. if (m.getMapSource() instanceof FileBasedMapSource) totalNrOfOnlineTiles -= m.calculateTilesToDownload(); } } if (totalNrOfOnlineTiles > 5000000) { // NumberFormat f = DecimalFormat.getInstance(); JOptionPane.showMessageDialog(null, String.format( I18nUtils.localizedStringForKey("msg_too_many_tiles_msg"), 5000000, totalNrOfOnlineTiles), I18nUtils .localizedStringForKey("msg_too_many_tiles_title"), JOptionPane.ERROR_MESSAGE); return; } try { atlasCreator.startAtlasCreation(atlas, customAtlasDir); } catch (AtlasTestException e) { JOptionPane.showMessageDialog(null, e.getMessage(), "Atlas format restriction violated", JOptionPane.ERROR_MESSAGE); return; } ap.initAtlas(atlas); ap.setVisible(true); Settings s = Settings.getInstance(); downloadJobDispatcher = new JobDispatcher(s.downloadThreadCount, pauseResumeHandler, ap); try { for (LayerInterface layer : atlas) { atlasCreator.initLayerCreation(layer); for (MapInterface map : layer) { try { while (!createMap(map)) ; } catch (InterruptedException e) { throw e; // User has aborted } catch (MapDownloadSkippedException e) { // Do nothing and continue with next map } catch (Exception e) { log.error("", e); String[] options = { I18nUtils.localizedStringForKey("Continue"), I18nUtils.localizedStringForKey("Abort"), I18nUtils.localizedStringForKey("dlg_download_show_error_report") }; int a = JOptionPane.showOptionDialog(null, I18nUtils.localizedStringForKey("dlg_download_erro_head") + e.getMessage() + "\n[" + e.getClass().getSimpleName() + "]\n\n", I18nUtils.localizedStringForKey("Error"), 0, JOptionPane.ERROR_MESSAGE, null, options, options[0]); switch (a) { case 2: GUIExceptionHandler.processException(e); case 1: throw new InterruptedException(); } } } atlasCreator.finishLayerCreation(); } } catch (InterruptedException e) { atlasCreator.abortAtlasCreation(); throw e; } catch (Error e) { atlasCreator.abortAtlasCreation(); throw e; } finally { // In case of an abort: Stop create new download jobs if (djp != null) djp.cancel(); downloadJobDispatcher.terminateAllWorkerThreads(); if (!atlasCreator.isAborted()) atlasCreator.finishAtlasCreation(); ap.atlasCreationFinished(); } } /** * * @param map * @return true if map creation process was finished and false if something went wrong and the user decided to retry * map download * @throws Exception */ public boolean createMap(MapInterface map) throws Exception { TarIndex tileIndex = null; TarIndexedArchive tileArchive = null; jobsCompleted = 0; jobsRetryError = 0; jobsPermanentError = 0; ap.initMapDownload(map); if (currentThread().isInterrupted()) throw new InterruptedException(); // Prepare the tile store directory // ts.prepareTileStore(map.getMapSource()); /*** * In this section of code below, tiles for Atlas is being downloaded and saved in the temporary layer tar file * in the system temp directory. **/ int zoom = map.getZoom(); final int tileCount = (int) map.calculateTilesToDownload(); ap.setZoomLevel(zoom); try { tileArchive = null; TileProvider mapTileProvider; if (!(map.getMapSource() instanceof FileBasedMapSource)) { // For online maps we download the tiles first and then start creating the map if // we are sure we got all tiles if (!AtlasOutputFormat.TILESTORE.equals(atlas.getOutputFormat())) { String tempSuffix = "MOBAC_" + atlas.getName() + "_" + zoom + "_"; File tempDir = null; String swapDir = Settings.getInstance().getSwapDir(); if (swapDir != null) { tempDir = new File(swapDir); if (!tempDir.exists()) tempDir = null; } if (tempDir == null) tempDir = DirectoryManager.tempDir; File tileArchiveFile = File.createTempFile(tempSuffix, ".tar", tempDir); // If something goes wrong the temp file only persists until the VM exits tileArchiveFile.deleteOnExit(); log.debug("Writing downloaded tiles to " + tileArchiveFile.getPath()); tileArchive = new TarIndexedArchive(tileArchiveFile, tileCount); } else log.debug("Downloading to tile store only"); djp = new DownloadJobProducerThread(this, downloadJobDispatcher, tileArchive, (DownloadableElement) map); boolean failedMessageAnswered = false; while (djp.isAlive() || (downloadJobDispatcher.getWaitingJobCount() > 0) || downloadJobDispatcher.isAtLeastOneWorkerActive()) { Thread.sleep(500); if (!failedMessageAnswered && (jobsRetryError > 50) && !ap.ignoreDownloadErrors()) { pauseResumeHandler.pause(); String[] answers = new String[] { I18nUtils.localizedStringForKey("Continue"), I18nUtils.localizedStringForKey("Retry"), I18nUtils.localizedStringForKey("Skip"), I18nUtils.localizedStringForKey("Abort") }; int answer = JOptionPane.showOptionDialog(ap, I18nUtils.localizedStringForKey("dlg_download_errors_todo_msg"), I18nUtils.localizedStringForKey("dlg_download_errors_todo"), 0, JOptionPane.QUESTION_MESSAGE, null, answers, answers[0]); failedMessageAnswered = true; switch (answer) { case 0: // Continue pauseResumeHandler.resume(); break; case 1: // Retry djp.cancel(); djp = null; downloadJobDispatcher.cancelOutstandingJobs(); return false; case 2: // Skip downloadJobDispatcher.cancelOutstandingJobs(); throw new MapDownloadSkippedException(); default: // Abort or close dialog downloadJobDispatcher.cancelOutstandingJobs(); downloadJobDispatcher.terminateAllWorkerThreads(); throw new InterruptedException(); } } } djp = null; log.debug("All download jobs has been completed!"); if (tileArchive != null) { tileArchive.writeEndofArchive(); tileArchive.close(); tileIndex = tileArchive.getTarIndex(); if (tileIndex.size() < tileCount && !ap.ignoreDownloadErrors()) { int missing = tileCount - tileIndex.size(); log.debug("Expected tile count: " + tileCount + " downloaded tile count: " + tileIndex.size() + " missing: " + missing); int answer = JOptionPane.showConfirmDialog(ap, String.format( I18nUtils.localizedStringForKey("dlg_download_errors_missing_tile_msg"), missing), I18nUtils.localizedStringForKey("dlg_download_errors_missing_tile"), JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE); if (answer != JOptionPane.YES_OPTION) throw new InterruptedException(); } } downloadJobDispatcher.cancelOutstandingJobs(); log.debug("Starting to create atlas from downloaded tiles"); mapTileProvider = new DownloadedTileProvider(tileIndex, map); } else { // We don't need to download anything. Everything is already stored locally therefore we can just use it mapTileProvider = new FilteredMapSourceProvider(map, LoadMethod.DEFAULT); } atlasCreator.initializeMap(map, mapTileProvider); atlasCreator.createMap(); } catch (Error e) { log.error("Error in createMap: " + e.getMessage(), e); throw e; } finally { if (tileIndex != null) tileIndex.closeAndDelete(); else if (tileArchive != null) tileArchive.delete(); } return true; } public void pauseResumeAtlasCreation() { if (pauseResumeHandler.isPaused()) { log.debug("Atlas creation resumed"); pauseResumeHandler.resume(); } else { log.debug("Atlas creation paused"); pauseResumeHandler.pause(); } } public boolean isPaused() { return pauseResumeHandler.isPaused(); } public PauseResumeHandler getPauseResumeHandler() { return pauseResumeHandler; } /** * Stop listener from {@link AtlasProgress} */ public void abortAtlasCreation() { try { DownloadJobProducerThread djp_ = djp; if (djp_ != null) djp_.cancel(); if (downloadJobDispatcher != null) downloadJobDispatcher.terminateAllWorkerThreads(); pauseResumeHandler.resume(); this.interrupt(); } catch (Exception e) { log.error("Exception thrown in stopDownload()" + e.getMessage()); } } public int getActiveDownloads() { return activeDownloads; } public synchronized void jobStarted() { activeDownloads++; } public void jobFinishedSuccessfully(int bytesDownloaded) { synchronized (this) { ap.incMapDownloadProgress(); activeDownloads--; jobsCompleted++; } ap.updateGUI(); } public void jobFinishedWithError(boolean retry) { synchronized (this) { activeDownloads--; if (retry) jobsRetryError++; else { jobsPermanentError++; ap.incMapDownloadProgress(); } } if (!ap.ignoreDownloadErrors()) Toolkit.getDefaultToolkit().beep(); ap.setErrorCounter(jobsRetryError, jobsPermanentError); ap.updateGUI(); } public int getMaxDownloadRetries() { return maxDownloadRetries; } public AtlasProgress getAtlasProgress() { return ap; } public File getCustomAtlasDir() { return customAtlasDir; } public void setCustomAtlasDir(File customAtlasDir) { this.customAtlasDir = customAtlasDir; } public void setQuitMobacAfterAtlasCreation(boolean quitMobacAfterAtlasCreation) { this.quitMobacAfterAtlasCreation = quitMobacAfterAtlasCreation; } }