]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/gui/map/DiskTileCacher.java
Version 20.4, May 2021
[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
42
43         /**
44          * Private constructor
45          * @param inUrl URL to get
46          * @param inFile file to save to
47          */
48         private DiskTileCacher(URL inUrl, File inFile, ImageObserver inObserver)
49         {
50                 _url = inUrl;
51                 _file = inFile;
52                 _observer = inObserver;
53                 _active = registerCacher(inFile.getAbsolutePath());
54         }
55
56         /**
57          * Get the specified tile from the disk cache
58          * @param inBasePath base path to whole disk cache
59          * @param inTilePath relative path to requested tile
60          * @return tile image if available, or null if not there
61          */
62         public static MapTile getTile(String inBasePath, String inTilePath)
63         {
64                 if (inBasePath == null) {return null;}
65                 File tileFile = new File(inBasePath, inTilePath);
66                 Image image = null;
67                 if (tileFile.exists() && tileFile.canRead() && tileFile.length() > 0)
68                 {
69                         long fileStamp = tileFile.lastModified();
70                         boolean isExpired = ((System.currentTimeMillis()-fileStamp) > CACHE_TIME_LIMIT);
71                         try
72                         {
73                                 image = Toolkit.getDefaultToolkit().createImage(tileFile.getAbsolutePath());
74                                 return new MapTile(image, isExpired);
75                         }
76                         catch (Exception e) {
77                                 System.err.println("createImage: " + e.getClass().getName() + " _ " + e.getMessage());
78                         }
79                 }
80                 return null;
81         }
82
83         /**
84          * Save the specified image tile to disk
85          * @param inUrl url to get image from
86          * @param inBasePath base path to disk cache
87          * @param inTilePath relative path to this tile
88          * @param inObserver observer to inform when load complete
89          */
90         public static void saveTile(URL inUrl, String inBasePath, String inTilePath, ImageObserver inObserver)
91         {
92                 if (inBasePath == null || inTilePath == null) {return;}
93                 // save file if possible
94                 File basePath = new File(inBasePath);
95                 if (!basePath.exists() || !basePath.isDirectory() || !basePath.canWrite())
96                 {
97                         // Can't write to base path
98                         return;
99                 }
100                 File tileFile = new File(basePath, inTilePath);
101
102                 // Check if it has already failed
103                 if (BLOCKED_URLS.contains(inUrl.toString())) {
104                         return;
105                 }
106
107                 File dir = tileFile.getParentFile();
108                 // Construct a cacher to load the image if necessary
109                 if ((dir.exists() || dir.mkdirs()) && dir.canWrite())
110                 {
111                         DiskTileCacher cacher = new DiskTileCacher(inUrl, tileFile, inObserver);
112                         cacher.startDownloading();
113                 }
114         }
115
116         /**
117          * Start downloading the configured tile
118          */
119         private void startDownloading()
120         {
121                 if (_active)
122                 {
123                         new Thread(this).start();
124                 }
125         }
126
127         /**
128          * Run method for loading URL asynchronously and saving to file
129          */
130         public void run()
131         {
132                 waitUntilAllowedToRun();
133                 if (doDownload())
134                 {
135                         if (!CONNECTION_ACTIVE)
136                         {
137                                 // wasn't active before but this download worked - we've come back online
138                                 BLOCKED_URLS.clear();
139                                 CONNECTION_ACTIVE = true;
140                         }
141                 }
142                 // Release file and thread
143                 unregisterCacher(_file.getAbsolutePath());
144                 threadFinished();
145         }
146
147         /**
148          * Blocks (in separate thread) until allowed by concurrent thread limit
149          */
150         private void waitUntilAllowedToRun()
151         {
152                 while (!canStartNewThread())
153                 {
154                         try {
155                                 Thread.sleep(400);
156                         }
157                         catch (InterruptedException e) {}
158                 }
159         }
160
161         /**
162          * @return true if download was successful
163          */
164         private boolean doDownload()
165         {
166                 boolean finished = false;
167                 InputStream in = null;
168                 FileOutputStream out = null;
169                 File tempFile = new File(_file.getAbsolutePath() + ".temp");
170
171                 if (tempFile.exists())
172                 {
173                         tempFile.delete();
174                 }
175                 try
176                 {
177                         if (!tempFile.createNewFile()) {return false;}
178                 }
179                 catch (Exception e) {return false;}
180
181                 try
182                 {
183                         // Open streams from URL and to file
184                         out = new FileOutputStream(tempFile);
185                         //System.out.println("DiskTileCacher opening URL: " + _url.toString());
186                         // Set http user agent on connection
187                         URLConnection conn = _url.openConnection();
188                         conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
189                         in = conn.getInputStream();
190                         int d = 0;
191                         // Loop over each byte in the stream (maybe buffering is more efficient?)
192                         while ((d = in.read()) >= 0) {
193                                 out.write(d);
194                         }
195                         finished = true;
196                 }
197                 catch (IOException e)
198                 {
199                         System.err.println("ioe: " + e.getClass().getName() + " - " + e.getMessage());
200                         BLOCKED_URLS.add(_url.toString());
201                         CONNECTION_ACTIVE = false;
202                 }
203                 finally
204                 {
205                         // clean up files
206                         try {in.close();} catch (Exception e) {} // ignore
207                         try {out.close();} catch (Exception e) {} // ignore
208                         if (!finished)
209                         {
210                                 tempFile.delete();
211                         }
212                 }
213                 boolean success = false;
214                 // Move temp file to desired file location
215                 if (tempFile.exists() && tempFile.length() > 0L)
216                 {
217                         if (tempFile.renameTo(_file))
218                         {
219                                 success = true;
220                         }
221                         else
222                         {
223                                 // File couldn't be moved - delete both to be sure
224                                 System.out.println("Failed to rename temp file: " + tempFile.getAbsolutePath());
225                                 tempFile.delete();
226                                 _file.delete();
227                         }
228                 }
229
230                 // Tell parent that load is finished (parameters ignored)
231                 _observer.imageUpdate(null, ImageObserver.ALLBITS, 0, 0, 0, 0);
232                 return success;
233         }
234
235         // Blocking of cachers working on same file
236
237         /**
238          * Register a cacher writing to the specified file path
239          * @param inFilePath destination path to tile file
240          * @return true if nobody else has claimed this file yet
241          */
242         private synchronized static boolean registerCacher(String inFilePath)
243         {
244                 if (DOWNLOADING_FILES.contains(inFilePath))
245                 {
246                         return false;
247                 }
248                 // Nobody has claimed this file yet
249                 DOWNLOADING_FILES.add(inFilePath);
250                 return true;
251         }
252
253         /**
254          * Cacher has finished dealing with the specified file
255          * @param inFilePath destination path to tile file
256          */
257         private synchronized static void unregisterCacher(String inFilePath)
258         {
259                 DOWNLOADING_FILES.remove(inFilePath);
260         }
261
262         // Limiting of active threads
263
264         /**
265          * @return true if another thread is allowed to become active
266          */
267         private synchronized static boolean canStartNewThread()
268         {
269                 final int MAXIMUM_NUM_THREADS = 8;
270                 if (NUMBER_ACTIVE_THREADS < MAXIMUM_NUM_THREADS)
271                 {
272                         NUMBER_ACTIVE_THREADS++;
273                         return true;
274                 }
275                 // Already too many threads active
276                 return false;
277         }
278
279         /**
280          * Inform that one of the previously active threads has now completed
281          */
282         private synchronized static void threadFinished()
283         {
284                 if (NUMBER_ACTIVE_THREADS > 0)
285                 {
286                         NUMBER_ACTIVE_THREADS--;
287                 }
288         }
289 }