]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/gui/map/DiskTileCacher.java
Version 20.1, December 2020
[GpsPrune.git] / src / tim / prune / gui / map / DiskTileCacher.java
1 package tim.prune.gui.map;
2
3 import java.awt.Image;
4 import java.awt.Toolkit;
5 import java.awt.image.ImageObserver;
6 import java.io.File;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.net.URL;
11 import java.net.URLConnection;
12 import java.util.HashSet;
13
14 import tim.prune.GpsPrune;
15
16 /**
17  * Class to control the reading and saving of map tiles
18  * to a cache on disk
19  */
20 public class DiskTileCacher implements Runnable
21 {
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;
30
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;
43
44
45         /**
46          * Private constructor
47          * @param inUrl URL to get
48          * @param inFile file to save to
49          */
50         private DiskTileCacher(URL inUrl, File inFile, ImageObserver inObserver)
51         {
52                 _url = inUrl;
53                 _file = inFile;
54                 _observer = inObserver;
55                 _active = registerCacher(inFile.getAbsolutePath());
56         }
57
58         /**
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
63          */
64         public static MapTile getTile(String inBasePath, String inTilePath)
65         {
66                 if (inBasePath == null) {return null;}
67                 File tileFile = new File(inBasePath, inTilePath);
68                 Image image = null;
69                 if (tileFile.exists() && tileFile.canRead() && tileFile.length() > 0)
70                 {
71                         long fileStamp = tileFile.lastModified();
72                         boolean isExpired = ((System.currentTimeMillis()-fileStamp) > CACHE_TIME_LIMIT);
73                         try
74                         {
75                                 image = Toolkit.getDefaultToolkit().createImage(tileFile.getAbsolutePath());
76                                 return new MapTile(image, isExpired);
77                         }
78                         catch (Exception e) {
79                                 System.err.println("createImage: " + e.getClass().getName() + " _ " + e.getMessage());
80                         }
81                 }
82                 return null;
83         }
84
85         /**
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
92          */
93         public static void saveTile(URL inUrl, String inBasePath, String inTilePath, ImageObserver inObserver) throws CacheFailure
94         {
95                 if (inBasePath == null || inTilePath == null) {return;}
96                 // save file if possible
97                 File basePath = new File(inBasePath);
98                 if (!checkBasePath(basePath))
99                 {
100                         // Can't write to base path
101                         throw new CacheFailure();
102                 }
103                 File tileFile = new File(basePath, inTilePath);
104
105                 // Check if it has already failed
106                 if (BLOCKED_URLS.contains(inUrl.toString())) {
107                         return;
108                 }
109
110                 File dir = tileFile.getParentFile();
111                 // Construct a cacher to load the image if necessary
112                 if ((dir.exists() || dir.mkdirs()) && dir.canWrite())
113                 {
114                         DiskTileCacher cacher = new DiskTileCacher(inUrl, tileFile, inObserver);
115                         cacher.startDownloading();
116                 }
117         }
118
119         /**
120          * Check the given base path, and try (once) to create it if necessary
121          * @return true if base path can be written to
122          */
123         private static boolean checkBasePath(File inBasePath)
124         {
125                 if (!inBasePath.exists() && !TRIED_TO_CREATE_BASEPATH)
126                 {
127                         TRIED_TO_CREATE_BASEPATH = true;
128                         System.out.println("Base path '" + inBasePath.getAbsolutePath() + "' does not exist, trying to create");
129                         return inBasePath.mkdirs();
130                 }
131                 return inBasePath.exists() && inBasePath.isDirectory() && inBasePath.canWrite();
132         }
133
134         /**
135          * Start downloading the configured tile
136          */
137         private void startDownloading()
138         {
139                 if (_active)
140                 {
141                         new Thread(this).start();
142                 }
143         }
144
145         /**
146          * Run method for loading URL asynchronously and saving to file
147          */
148         public void run()
149         {
150                 waitUntilAllowedToRun();
151                 if (doDownload())
152                 {
153                         if (!CONNECTION_ACTIVE)
154                         {
155                                 // wasn't active before but this download worked - we've come back online
156                                 BLOCKED_URLS.clear();
157                                 CONNECTION_ACTIVE = true;
158                         }
159                 }
160                 // Release file and thread
161                 unregisterCacher(_file.getAbsolutePath());
162                 threadFinished();
163         }
164
165         /**
166          * Blocks (in separate thread) until allowed by concurrent thread limit
167          */
168         private void waitUntilAllowedToRun()
169         {
170                 while (!canStartNewThread())
171                 {
172                         try {
173                                 Thread.sleep(400);
174                         }
175                         catch (InterruptedException e) {}
176                 }
177         }
178
179         /**
180          * @return true if download was successful
181          */
182         private boolean doDownload()
183         {
184                 boolean finished = false;
185                 InputStream in = null;
186                 FileOutputStream out = null;
187                 File tempFile = new File(_file.getAbsolutePath() + ".temp");
188
189                 if (tempFile.exists())
190                 {
191                         tempFile.delete();
192                 }
193                 try
194                 {
195                         if (!tempFile.createNewFile()) {return false;}
196                 }
197                 catch (Exception e) {return false;}
198
199                 try
200                 {
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();
208                         int d = 0;
209                         // Loop over each byte in the stream (maybe buffering is more efficient?)
210                         while ((d = in.read()) >= 0) {
211                                 out.write(d);
212                         }
213                         finished = true;
214                 }
215                 catch (IOException e)
216                 {
217                         System.err.println("ioe: " + e.getClass().getName() + " - " + e.getMessage());
218                         BLOCKED_URLS.add(_url.toString());
219                         CONNECTION_ACTIVE = false;
220                 }
221                 finally
222                 {
223                         // clean up files
224                         try {in.close();} catch (Exception e) {} // ignore
225                         try {out.close();} catch (Exception e) {} // ignore
226                         if (!finished)
227                         {
228                                 tempFile.delete();
229                         }
230                 }
231                 boolean success = false;
232                 // Move temp file to desired file location
233                 if (tempFile.exists() && tempFile.length() > 0L)
234                 {
235                         if (tempFile.renameTo(_file))
236                         {
237                                 success = true;
238                         }
239                         else if (_file.delete() && tempFile.renameTo(_file))
240                         {
241                                 success = true;
242                         }
243                         else
244                         {
245                                 System.out.println("Failed to rename temp file: " + tempFile.getAbsolutePath());
246                                 tempFile.delete();
247                         }
248                 }
249
250                 // Tell parent that load is finished (parameters ignored)
251                 _observer.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, 0, 0);
252                 return success;
253         }
254
255         // Blocking of cachers working on same file
256
257         /**
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
261          */
262         private synchronized static boolean registerCacher(String inFilePath)
263         {
264                 if (DOWNLOADING_FILES.contains(inFilePath))
265                 {
266                         return false;
267                 }
268                 // Nobody has claimed this file yet
269                 DOWNLOADING_FILES.add(inFilePath);
270                 return true;
271         }
272
273         /**
274          * Cacher has finished dealing with the specified file
275          * @param inFilePath destination path to tile file
276          */
277         private synchronized static void unregisterCacher(String inFilePath)
278         {
279                 DOWNLOADING_FILES.remove(inFilePath);
280         }
281
282         // Limiting of active threads
283
284         /**
285          * @return true if another thread is allowed to become active
286          */
287         private synchronized static boolean canStartNewThread()
288         {
289                 final int MAXIMUM_NUM_THREADS = 8;
290                 if (NUMBER_ACTIVE_THREADS < MAXIMUM_NUM_THREADS)
291                 {
292                         NUMBER_ACTIVE_THREADS++;
293                         return true;
294                 }
295                 // Already too many threads active
296                 return false;
297         }
298
299         /**
300          * Inform that one of the previously active threads has now completed
301          */
302         private synchronized static void threadFinished()
303         {
304                 if (NUMBER_ACTIVE_THREADS > 0)
305                 {
306                         NUMBER_ACTIVE_THREADS--;
307                 }
308         }
309 }