1 package tim.prune.gui.map;
4 import java.awt.Toolkit;
5 import java.awt.image.ImageObserver;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
11 import java.net.URLConnection;
12 import java.util.HashSet;
14 import tim.prune.GpsPrune;
17 * Class to control the reading and saving of map tiles
20 public class DiskTileCacher implements Runnable
22 /** URL to get image from */
23 private URL _url = null;
24 /** File to save image to */
25 private File _file = null;
26 /** Observer to be notified */
27 private ImageObserver _observer = null;
28 /** True if cacher is active, false if blocked */
29 private boolean _active = false;
31 /** Time limit to cache images for */
32 private static final long CACHE_TIME_LIMIT = 20 * 24 * 60 * 60 * 1000; // 20 days in ms
33 /** Hashset of all blocked / 404 tiles to avoid requesting them again */
34 private static final HashSet<String> BLOCKED_URLS = new HashSet<String>();
35 /**Hashset of files which are currently being processed */
36 private static final HashSet<String> DOWNLOADING_FILES = new HashSet<String>();
37 /** Number of currently active threads */
38 private static int NUMBER_ACTIVE_THREADS = 0;
39 /** Flag to remember whether any server connection is possible */
40 private static boolean CONNECTION_ACTIVE = true;
41 /** Flag to remember whether we have already tried to create the base path */
42 private static boolean TRIED_TO_CREATE_BASEPATH = false;
47 * @param inUrl URL to get
48 * @param inFile file to save to
50 private DiskTileCacher(URL inUrl, File inFile, ImageObserver inObserver)
54 _observer = inObserver;
55 _active = registerCacher(inFile.getAbsolutePath());
59 * Get the specified tile from the disk cache
60 * @param inBasePath base path to whole disk cache
61 * @param inTilePath relative path to requested tile
62 * @return tile image if available, or null if not there
64 public static MapTile getTile(String inBasePath, String inTilePath)
66 if (inBasePath == null) {return null;}
67 File tileFile = new File(inBasePath, inTilePath);
69 if (tileFile.exists() && tileFile.canRead() && tileFile.length() > 0)
71 long fileStamp = tileFile.lastModified();
72 boolean isExpired = ((System.currentTimeMillis()-fileStamp) > CACHE_TIME_LIMIT);
75 image = Toolkit.getDefaultToolkit().createImage(tileFile.getAbsolutePath());
76 return new MapTile(image, isExpired);
79 System.err.println("createImage: " + e.getClass().getName() + " _ " + e.getMessage());
86 * Save the specified image tile to disk
87 * @param inUrl url to get image from
88 * @param inBasePath base path to disk cache
89 * @param inTilePath relative path to this tile
90 * @param inObserver observer to inform when load complete
91 * @throws CacheFailure if tile could not be saved
93 public static void saveTile(URL inUrl, String inBasePath, String inTilePath, ImageObserver inObserver) throws CacheFailure
95 if (inBasePath == null || inTilePath == null) {return;}
96 // save file if possible
97 File basePath = new File(inBasePath);
98 if (!checkBasePath(basePath))
100 // Can't write to base path
101 throw new CacheFailure();
103 File tileFile = new File(basePath, inTilePath);
105 // Check if it has already failed
106 if (BLOCKED_URLS.contains(inUrl.toString())) {
110 File dir = tileFile.getParentFile();
111 // Construct a cacher to load the image if necessary
112 if ((dir.exists() || dir.mkdirs()) && dir.canWrite())
114 DiskTileCacher cacher = new DiskTileCacher(inUrl, tileFile, inObserver);
115 cacher.startDownloading();
120 * Check the given base path, and try (once) to create it if necessary
121 * @return true if base path can be written to
123 private static boolean checkBasePath(File inBasePath)
125 if (!inBasePath.exists() && !TRIED_TO_CREATE_BASEPATH)
127 TRIED_TO_CREATE_BASEPATH = true;
128 System.out.println("Base path '" + inBasePath.getAbsolutePath() + "' does not exist, trying to create");
129 return inBasePath.mkdirs();
131 return inBasePath.exists() && inBasePath.isDirectory() && inBasePath.canWrite();
135 * Start downloading the configured tile
137 private void startDownloading()
141 new Thread(this).start();
146 * Run method for loading URL asynchronously and saving to file
150 waitUntilAllowedToRun();
153 if (!CONNECTION_ACTIVE)
155 // wasn't active before but this download worked - we've come back online
156 BLOCKED_URLS.clear();
157 CONNECTION_ACTIVE = true;
160 // Release file and thread
161 unregisterCacher(_file.getAbsolutePath());
166 * Blocks (in separate thread) until allowed by concurrent thread limit
168 private void waitUntilAllowedToRun()
170 while (!canStartNewThread())
175 catch (InterruptedException e) {}
180 * @return true if download was successful
182 private boolean doDownload()
184 boolean finished = false;
185 InputStream in = null;
186 FileOutputStream out = null;
187 File tempFile = new File(_file.getAbsolutePath() + ".temp");
189 if (tempFile.exists())
195 if (!tempFile.createNewFile()) {return false;}
197 catch (Exception e) {return false;}
201 // Open streams from URL and to file
202 out = new FileOutputStream(tempFile);
203 //System.out.println("DiskTileCacher opening URL: " + _url.toString());
204 // Set http user agent on connection
205 URLConnection conn = _url.openConnection();
206 conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
207 in = conn.getInputStream();
209 // Loop over each byte in the stream (maybe buffering is more efficient?)
210 while ((d = in.read()) >= 0) {
215 catch (IOException e)
217 System.err.println("ioe: " + e.getClass().getName() + " - " + e.getMessage());
218 BLOCKED_URLS.add(_url.toString());
219 CONNECTION_ACTIVE = false;
224 try {in.close();} catch (Exception e) {} // ignore
225 try {out.close();} catch (Exception e) {} // ignore
231 boolean success = false;
232 // Move temp file to desired file location
233 if (tempFile.exists() && tempFile.length() > 0L)
235 if (tempFile.renameTo(_file))
239 else if (_file.delete() && tempFile.renameTo(_file))
245 System.out.println("Failed to rename temp file: " + tempFile.getAbsolutePath());
250 // Tell parent that load is finished (parameters ignored)
251 _observer.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, 0, 0);
255 // Blocking of cachers working on same file
258 * Register a cacher writing to the specified file path
259 * @param inFilePath destination path to tile file
260 * @return true if nobody else has claimed this file yet
262 private synchronized static boolean registerCacher(String inFilePath)
264 if (DOWNLOADING_FILES.contains(inFilePath))
268 // Nobody has claimed this file yet
269 DOWNLOADING_FILES.add(inFilePath);
274 * Cacher has finished dealing with the specified file
275 * @param inFilePath destination path to tile file
277 private synchronized static void unregisterCacher(String inFilePath)
279 DOWNLOADING_FILES.remove(inFilePath);
282 // Limiting of active threads
285 * @return true if another thread is allowed to become active
287 private synchronized static boolean canStartNewThread()
289 final int MAXIMUM_NUM_THREADS = 8;
290 if (NUMBER_ACTIVE_THREADS < MAXIMUM_NUM_THREADS)
292 NUMBER_ACTIVE_THREADS++;
295 // Already too many threads active
300 * Inform that one of the previously active threads has now completed
302 private synchronized static void threadFinished()
304 if (NUMBER_ACTIVE_THREADS > 0)
306 NUMBER_ACTIVE_THREADS--;