]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/gui/map/DiskTileCacher.java
Version 20.4, May 2021
[GpsPrune.git] / src / tim / prune / gui / map / DiskTileCacher.java
index fd2042a7714acfe2ca1f5b47693ce9df06b0a79f..9be7841372299894abd9a26ca85ea149ee9ad8fa 100644 (file)
@@ -25,10 +25,22 @@ public class DiskTileCacher implements Runnable
        private File _file = null;
        /** Observer to be notified */
        private ImageObserver _observer = null;
+       /** True if cacher is active, false if blocked */
+       private boolean _active = false;
+
        /** Time limit to cache images for */
        private static final long CACHE_TIME_LIMIT = 20 * 24 * 60 * 60 * 1000; // 20 days in ms
        /** Hashset of all blocked / 404 tiles to avoid requesting them again */
        private static final HashSet<String> BLOCKED_URLS = new HashSet<String>();
+       /**Hashset of files which are currently being processed */
+       private static final HashSet<String> DOWNLOADING_FILES = new HashSet<String>();
+       /** Number of currently active threads */
+       private static int NUMBER_ACTIVE_THREADS = 0;
+       /** Flag to remember whether any server connection is possible */
+       private static boolean CONNECTION_ACTIVE = true;
+       /** Flag to remember whether we have already tried to create the base path */
+       private static boolean TRIED_TO_CREATE_BASEPATH = false;
+
 
        /**
         * Private constructor
@@ -40,6 +52,7 @@ public class DiskTileCacher implements Runnable
                _url = inUrl;
                _file = inFile;
                _observer = inObserver;
+               _active = registerCacher(inFile.getAbsolutePath());
        }
 
        /**
@@ -75,69 +88,119 @@ public class DiskTileCacher implements Runnable
         * @param inBasePath base path to disk cache
         * @param inTilePath relative path to this tile
         * @param inObserver observer to inform when load complete
+        * @throws CacheFailure if tile could not be saved
         */
-       public static void saveTile(URL inUrl, String inBasePath, String inTilePath, ImageObserver inObserver)
+       public static void saveTile(URL inUrl, String inBasePath, String inTilePath, ImageObserver inObserver) throws CacheFailure
        {
                if (inBasePath == null || inTilePath == null) {return;}
                // save file if possible
                File basePath = new File(inBasePath);
-               if (!basePath.exists() || !basePath.isDirectory() || !basePath.canWrite()) {
+               if (!checkBasePath(basePath))
+               {
                        // Can't write to base path
-                       return;
+                       throw new CacheFailure();
                }
                File tileFile = new File(basePath, inTilePath);
-               // Check if this file is already being loaded
-               if (isBeingLoaded(tileFile)) {return;}
+
                // Check if it has already failed
-               if (BLOCKED_URLS.contains(inUrl.toString())) {return;}
+               if (BLOCKED_URLS.contains(inUrl.toString())) {
+                       return;
+               }
 
                File dir = tileFile.getParentFile();
-               // Start a new thread to load the image if necessary
+               // Construct a cacher to load the image if necessary
                if ((dir.exists() || dir.mkdirs()) && dir.canWrite())
                {
-                       new Thread(new DiskTileCacher(inUrl, tileFile, inObserver)).start();
+                       DiskTileCacher cacher = new DiskTileCacher(inUrl, tileFile, inObserver);
+                       cacher.startDownloading();
                }
        }
 
        /**
-        * Check whether the given tile is already being loaded
-        * @param inFile desired file
-        * @return true if temporary file with this name exists
+        * Check the given base path, and try (once) to create it if necessary
+        * @return true if base path can be written to
         */
-       private static boolean isBeingLoaded(File inFile)
+       private static boolean checkBasePath(File inBasePath)
        {
-               File tempFile = new File(inFile.getAbsolutePath() + ".temp");
-               if (!tempFile.exists()) {
-                       return false;
+               if (!inBasePath.exists() && !TRIED_TO_CREATE_BASEPATH)
+               {
+                       TRIED_TO_CREATE_BASEPATH = true;
+                       System.out.println("Base path '" + inBasePath.getAbsolutePath() + "' does not exist, trying to create");
+                       return inBasePath.mkdirs();
+               }
+               return inBasePath.exists() && inBasePath.isDirectory() && inBasePath.canWrite();
+       }
+
+       /**
+        * Start downloading the configured tile
+        */
+       private void startDownloading()
+       {
+               if (_active)
+               {
+                       new Thread(this).start();
                }
-               // File exists, so check if it was created recently
-               final long fileAge = System.currentTimeMillis() - tempFile.lastModified();
-               return fileAge < 1000000L; // overwrite if the temp file is still there after 1000s
        }
 
        /**
         * Run method for loading URL asynchronously and saving to file
         */
        public void run()
+       {
+               waitUntilAllowedToRun();
+               if (doDownload())
+               {
+                       if (!CONNECTION_ACTIVE)
+                       {
+                               // wasn't active before but this download worked - we've come back online
+                               BLOCKED_URLS.clear();
+                               CONNECTION_ACTIVE = true;
+                       }
+               }
+               // Release file and thread
+               unregisterCacher(_file.getAbsolutePath());
+               threadFinished();
+       }
+
+       /**
+        * Blocks (in separate thread) until allowed by concurrent thread limit
+        */
+       private void waitUntilAllowedToRun()
+       {
+               while (!canStartNewThread())
+               {
+                       try {
+                               Thread.sleep(400);
+                       }
+                       catch (InterruptedException e) {}
+               }
+       }
+
+       /**
+        * @return true if download was successful
+        */
+       private boolean doDownload()
        {
                boolean finished = false;
                InputStream in = null;
                FileOutputStream out = null;
                File tempFile = new File(_file.getAbsolutePath() + ".temp");
-               // Use a synchronized block across all threads to make sure this url is only fetched once
-               synchronized (DiskTileCacher.class)
+
+               if (tempFile.exists())
                {
-                       if (tempFile.exists()) {tempFile.delete();}
-                       try {
-                               if (!tempFile.createNewFile()) {return;}
-                       }
-                       catch (Exception e) {return;}
+                       tempFile.delete();
                }
+               try
+               {
+                       if (!tempFile.createNewFile()) {return false;}
+               }
+               catch (Exception e) {return false;}
+
                try
                {
                        // Open streams from URL and to file
                        out = new FileOutputStream(tempFile);
-                       //System.out.println("Opening URL: " + _url.toString());
+                       //System.out.println("DiskTileCacher opening URL: " + _url.toString());
                        // Set http user agent on connection
                        URLConnection conn = _url.openConnection();
                        conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
@@ -148,27 +211,99 @@ public class DiskTileCacher implements Runnable
                                out.write(d);
                        }
                        finished = true;
-               } catch (IOException e) {
+               }
+               catch (IOException e)
+               {
                        System.err.println("ioe: " + e.getClass().getName() + " - " + e.getMessage());
                        BLOCKED_URLS.add(_url.toString());
+                       CONNECTION_ACTIVE = false;
                }
                finally
                {
                        // clean up files
                        try {in.close();} catch (Exception e) {} // ignore
                        try {out.close();} catch (Exception e) {} // ignore
-                       if (!finished) {
+                       if (!finished)
+                       {
                                tempFile.delete();
                        }
                }
+               boolean success = false;
                // Move temp file to desired file location
-               if (tempFile.exists() && !tempFile.renameTo(_file))
+               if (tempFile.exists() && tempFile.length() > 0L)
                {
-                       // File couldn't be moved - delete both to be sure
-                       tempFile.delete();
-                       _file.delete();
+                       if (tempFile.renameTo(_file))
+                       {
+                               success = true;
+                       }
+                       else if (_file.delete() && tempFile.renameTo(_file))
+                       {
+                               success = true;
+                       }
+                       else
+                       {
+                               System.out.println("Failed to rename temp file: " + tempFile.getAbsolutePath());
+                               tempFile.delete();
+                       }
                }
+
                // Tell parent that load is finished (parameters ignored)
                _observer.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, 0, 0);
+               return success;
+       }
+
+       // Blocking of cachers working on same file
+
+       /**
+        * Register a cacher writing to the specified file path
+        * @param inFilePath destination path to tile file
+        * @return true if nobody else has claimed this file yet
+        */
+       private synchronized static boolean registerCacher(String inFilePath)
+       {
+               if (DOWNLOADING_FILES.contains(inFilePath))
+               {
+                       return false;
+               }
+               // Nobody has claimed this file yet
+               DOWNLOADING_FILES.add(inFilePath);
+               return true;
+       }
+
+       /**
+        * Cacher has finished dealing with the specified file
+        * @param inFilePath destination path to tile file
+        */
+       private synchronized static void unregisterCacher(String inFilePath)
+       {
+               DOWNLOADING_FILES.remove(inFilePath);
+       }
+
+       // Limiting of active threads
+
+       /**
+        * @return true if another thread is allowed to become active
+        */
+       private synchronized static boolean canStartNewThread()
+       {
+               final int MAXIMUM_NUM_THREADS = 8;
+               if (NUMBER_ACTIVE_THREADS < MAXIMUM_NUM_THREADS)
+               {
+                       NUMBER_ACTIVE_THREADS++;
+                       return true;
+               }
+               // Already too many threads active
+               return false;
+       }
+
+       /**
+        * Inform that one of the previously active threads has now completed
+        */
+       private synchronized static void threadFinished()
+       {
+               if (NUMBER_ACTIVE_THREADS > 0)
+               {
+                       NUMBER_ACTIVE_THREADS--;
+               }
        }
 }