]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/function/srtm/LookupSrtmFunction.java
LookupSrtmFunction: always set point altitude, even if points already have an altitude
[GpsPrune.git] / src / tim / prune / function / srtm / LookupSrtmFunction.java
1 package tim.prune.function.srtm;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.IOException;
6 import java.net.URL;
7 import java.util.HashSet;
8 import java.util.zip.ZipEntry;
9 import java.util.zip.ZipInputStream;
10
11 import javax.swing.JOptionPane;
12
13 import tim.prune.App;
14 import tim.prune.DataSubscriber;
15 import tim.prune.GenericFunction;
16 import tim.prune.I18nManager;
17 import tim.prune.UpdateMessageBroker;
18 import tim.prune.config.Config;
19 import tim.prune.data.Altitude;
20 import tim.prune.data.DataPoint;
21 import tim.prune.data.Field;
22 import tim.prune.data.Track;
23 import tim.prune.data.UnitSetLibrary;
24 import tim.prune.gui.ProgressDialog;
25 import tim.prune.tips.TipManager;
26 import tim.prune.undo.UndoLookupSrtm;
27
28 /**
29  * Class to provide a lookup function for point altitudes using the Space
30  * Shuttle's SRTM data files. HGT files are downloaded into memory via HTTP and
31  * point altitudes can then be interpolated from the 3m grid data.
32  */
33 public class LookupSrtmFunction extends GenericFunction implements Runnable
34 {
35         /** Progress dialog */
36         private ProgressDialog _progress = null;
37         /** Track to process */
38         private Track _track = null;
39         /** Flag for whether this is a real track or a terrain one */
40         private boolean _normalTrack = true;
41         /** Flag set when any tiles had to be downloaded (rather than just loaded locally) */
42         private boolean _hadToDownload = false;
43         /** Count the number of tiles downloaded and cached */
44         private int _numCached = 0;
45         /** Flag to check whether this function is currently running or not */
46         private boolean _running = false;
47
48         /** Expected size of hgt file in bytes */
49         private static final long HGT_SIZE = 2884802L;
50         /** Altitude below which is considered void */
51         private static final int VOID_VAL = -32768;
52
53         /**
54          * Constructor
55          * @param inApp  App object
56          */
57         public LookupSrtmFunction(App inApp) {
58                 super(inApp);
59         }
60
61         /** @return name key */
62         public String getNameKey() {
63                 return "function.lookupsrtm";
64         }
65
66         /**
67          * Begin the lookup using the normal track
68          */
69         public void begin() {
70                 begin(_app.getTrackInfo().getTrack(), true);
71         }
72
73         /**
74          * Begin the lookup with an alternative track
75          * @param inAlternativeTrack
76          */
77         public void begin(Track inAlternativeTrack) {
78                 begin(inAlternativeTrack, false);
79         }
80
81         /**
82          * Begin the function with the given parameters
83          * @param inTrack track to process
84          * @param inNormalTrack true if this is a "normal" track, false for an artificially constructed one such as for terrain
85          */
86         private void begin(Track inTrack, boolean inNormalTrack)
87         {
88                 _running = true;
89                 _hadToDownload = false;
90                 if (_progress == null) {
91                         _progress = new ProgressDialog(_parentFrame, getNameKey());
92                 }
93                 _progress.show();
94                 _track = inTrack;
95                 _normalTrack = inNormalTrack;
96                 // start new thread for time-consuming part
97                 new Thread(this).start();
98         }
99
100         /**
101          * Run method using separate thread
102          */
103         public void run()
104         {
105                 // Now loop again to extract the required tiles
106                 HashSet<SrtmTile> tileSet = new HashSet<SrtmTile>();
107                 for (int i = 0; i < _track.getNumPoints(); i++)
108                 {
109                         tileSet.add(new SrtmTile(_track.getPoint(i)));
110                 }
111                 lookupValues(tileSet);
112                 // Finished
113                 _running = false;
114                 // Show tip if lots of online lookups were necessary
115                 if (_hadToDownload) {
116                         _app.showTip(TipManager.Tip_DownloadSrtm);
117                 }
118                 else if (_numCached > 0) {
119                         showConfirmMessage(_numCached);
120                 }
121         }
122
123
124         /**
125          * Lookup the values from SRTM data
126          * @param inTileSet set of tiles to get
127          */
128         private void lookupValues(HashSet<SrtmTile> inTileSet)
129         {
130                 UndoLookupSrtm undo = new UndoLookupSrtm(_app.getTrackInfo());
131                 int numAltitudesFound = 0;
132                 TileFinder tileFinder = new TileFinder();
133                 String errorMessage = null;
134                 final int numTiles = inTileSet.size();
135
136                 // Update progress bar
137                 if (_progress != null)
138                 {
139                         _progress.setMaximum(numTiles);
140                         _progress.setValue(0);
141                 }
142                 int currentTileIndex = 0;
143                 _numCached = 0;
144                 for (SrtmTile tile : inTileSet)
145                 {
146                         URL url = tileFinder.getUrl(tile);
147                         if (url != null)
148                         {
149                                 try
150                                 {
151                                         // Set progress
152                                         _progress.setValue(currentTileIndex++);
153                                         final int ARRLENGTH = 1201 * 1201;
154                                         int[] heights = new int[ARRLENGTH];
155                                         // Open zipinputstream on url and check size
156                                         ZipInputStream inStream = getStreamToSrtmData(url);
157                                         boolean entryOk = false;
158                                         if (inStream != null)
159                                         {
160                                                 ZipEntry entry = inStream.getNextEntry();
161                                                 entryOk = (entry != null && entry.getSize() == HGT_SIZE);
162                                                 if (entryOk)
163                                                 {
164                                                         // Read entire file contents into one byte array
165                                                         for (int i = 0; i < ARRLENGTH; i++)
166                                                         {
167                                                                 heights[i] = inStream.read() * 256 + inStream.read();
168                                                                 if (heights[i] >= 32768) {heights[i] -= 65536;}
169                                                         }
170                                                 }
171                                                 // else {
172                                                 //      System.out.println("length not ok: " + entry.getSize());
173                                                 // }
174                                                 // Close stream from url
175                                                 inStream.close();
176                                         }
177
178                                         if (entryOk)
179                                         {
180                                                 numAltitudesFound += applySrtmTileToWholeTrack(tile, heights);
181                                         }
182                                 }
183                                 catch (IOException ioe) {
184                                         errorMessage = ioe.getClass().getName() + " - " + ioe.getMessage();
185                                 }
186                         }
187                 }
188
189                 _progress.dispose();
190                 if (_progress.isCancelled()) {
191                         return;
192                 }
193
194                 if (numAltitudesFound > 0)
195                 {
196                         // Inform app including undo information
197                         _track.requestRescale();
198                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
199                         // Don't update app if we're doing another track
200                         if (_normalTrack)
201                         {
202                                 _app.completeFunction(undo,
203                                         I18nManager.getTextWithNumber("confirm.lookupsrtm", numAltitudesFound));
204                         }
205                 }
206                 else if (errorMessage != null) {
207                         _app.showErrorMessageNoLookup(getNameKey(), errorMessage);
208                 }
209                 else if (numTiles > 0) {
210                         _app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonefound");
211                 }
212                 else {
213                         _app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonerequired");
214                 }
215         }
216
217         /**
218          * See whether the SRTM file is already available locally first, then try online
219          * @param inUrl URL for online resource
220          * @return ZipInputStream either on the local file or on the downloaded zip file
221          */
222         private ZipInputStream getStreamToSrtmData(URL inUrl)
223         throws IOException
224         {
225                 ZipInputStream localData = null;
226                 try {
227                         localData = getStreamToLocalHgtFile(inUrl);
228                 }
229                 catch (IOException ioe) {
230                         localData = null;
231                 }
232                 if (localData != null)
233                 {
234                         return localData;
235                 }
236                 // try to download to cache
237                 TileDownloader cacher = new TileDownloader();
238                 TileDownloader.Result result = cacher.downloadTile(inUrl);
239                 // System.out.println("Result: " + result);
240                 if (result == TileDownloader.Result.DOWNLOADED)
241                 {
242                         _numCached++;
243                         return getStreamToLocalHgtFile(inUrl);
244                 }
245                 // If we don't have a cache, we may be able to download it temporarily
246                 if (result != TileDownloader.Result.DOWNLOAD_FAILED)
247                 {
248                         _hadToDownload = true;
249                         return new ZipInputStream(inUrl.openStream());
250                 }
251                 // everything failed
252                 return null;
253         }
254
255         /**
256          * Get the SRTM file from the local cache, if available
257          * @param inUrl URL for online resource
258          * @return ZipInputStream on the local file or null if not there
259          */
260         private ZipInputStream getStreamToLocalHgtFile(URL inUrl)
261         throws IOException
262         {
263                 String diskCachePath = Config.getConfigString(Config.KEY_DISK_CACHE);
264                 if (diskCachePath != null)
265                 {
266                         File srtmDir = new File(diskCachePath, "srtm");
267                         if (srtmDir.exists() && srtmDir.isDirectory() && srtmDir.canRead())
268                         {
269                                 File srtmFile = new File(srtmDir, new File(inUrl.getFile()).getName());
270                                 if (srtmFile.exists() && srtmFile.isFile() && srtmFile.canRead()
271                                         && srtmFile.length() > 400)
272                                 {
273                                         // System.out.println("Lookup: Using file " + srtmFile.getAbsolutePath());
274                                         // File found, use this one
275                                         return new ZipInputStream(new FileInputStream(srtmFile));
276                                 }
277                         }
278                 }
279                 return null;
280         }
281
282         /**
283          * Given the height data read in from file, apply the given tile to all points
284          * in the track with missing altitude
285          * @param inTile tile being applied
286          * @param inHeights height data read in from file
287          * @return number of altitudes found
288          */
289         private int applySrtmTileToWholeTrack(SrtmTile inTile, int[] inHeights)
290         {
291                 int numAltitudesFound = 0;
292                 // Loop over all points in track, try to apply altitude from array
293                 for (int p = 0; p < _track.getNumPoints(); p++)
294                 {
295                         DataPoint point = _track.getPoint(p);
296                         if (new SrtmTile(point).equals(inTile))
297                         {
298                                 double x = (point.getLongitude().getDouble() - inTile.getLongitude()) * 1200;
299                                 double y = 1201 - (point.getLatitude().getDouble() - inTile.getLatitude()) * 1200;
300                                 int idx1 = ((int)y)*1201 + (int)x;
301                                 try
302                                 {
303                                         int[] fouralts = {inHeights[idx1], inHeights[idx1+1], inHeights[idx1-1201], inHeights[idx1-1200]};
304                                         int numVoids = (fouralts[0]==VOID_VAL?1:0) + (fouralts[1]==VOID_VAL?1:0)
305                                                 + (fouralts[2]==VOID_VAL?1:0) + (fouralts[3]==VOID_VAL?1:0);
306                                         // if (numVoids > 0) System.out.println(numVoids + " voids found");
307                                         double altitude = 0.0;
308                                         switch (numVoids)
309                                         {
310                                         case 0: altitude = bilinearInterpolate(fouralts, x, y); break;
311                                         case 1: altitude = bilinearInterpolate(fixVoid(fouralts), x, y); break;
312                                         case 2:
313                                         case 3: altitude = averageNonVoid(fouralts); break;
314                                         default: altitude = VOID_VAL;
315                                         }
316                                         // Special case for terrain tracks, don't interpolate voids yet
317                                         if (!_normalTrack && numVoids > 0) {
318                                                 altitude = VOID_VAL;
319                                         }
320                                         if (altitude != VOID_VAL)
321                                         {
322                                                 point.setFieldValue(Field.ALTITUDE, ""+altitude, false);
323                                                 // depending on settings, this value may have been added as feet, we need to force metres
324                                                 point.getAltitude().reset(new Altitude((int)altitude, UnitSetLibrary.UNITS_METRES));
325                                                 numAltitudesFound++;
326                                         }
327                                 }
328                                 catch (ArrayIndexOutOfBoundsException obe) {
329                                         // System.err.println("lat=" + point.getLatitude().getDouble() + ", x=" + x + ", y=" + y + ", idx=" + idx1);
330                                 }
331                         }
332                 }
333                 return numAltitudesFound;
334         }
335
336         /**
337          * Perform a bilinear interpolation on the given altitude array
338          * @param inAltitudes array of four altitude values on corners of square (bl, br, tl, tr)
339          * @param inX x coordinate
340          * @param inY y coordinate
341          * @return interpolated altitude
342          */
343         private static double bilinearInterpolate(int[] inAltitudes, double inX, double inY)
344         {
345                 double alpha = inX - (int) inX;
346                 double beta  = 1 - (inY - (int) inY);
347                 double alt = (1-alpha)*(1-beta)*inAltitudes[0] + alpha*(1-beta)*inAltitudes[1]
348                         + (1-alpha)*beta*inAltitudes[2] + alpha*beta*inAltitudes[3];
349                 return alt;
350         }
351
352         /**
353          * Fix a single void in the given array by replacing it with the average of the others
354          * @param inAltitudes array of altitudes containing one void
355          * @return fixed array without voids
356          */
357         private static int[] fixVoid(int[] inAltitudes)
358         {
359                 int[] fixed = new int[inAltitudes.length];
360                 for (int i = 0; i < inAltitudes.length; i++)
361                 {
362                         if (inAltitudes[i] == VOID_VAL) {
363                                 fixed[i] = (int) Math.round(averageNonVoid(inAltitudes));
364                         }
365                         else {
366                                 fixed[i] = inAltitudes[i];
367                         }
368                 }
369                 return fixed;
370         }
371
372         /**
373          * Calculate the average of the non-void altitudes in the given array
374          * @param inAltitudes array of altitudes with one or more voids
375          * @return average of non-void altitudes
376          */
377         private static final double averageNonVoid(int[] inAltitudes)
378         {
379                 double totalAltitude = 0.0;
380                 int numAlts = 0;
381                 for (int i = 0; i < inAltitudes.length; i++)
382                 {
383                         if (inAltitudes[i] != VOID_VAL)
384                         {
385                                 totalAltitude += inAltitudes[i];
386                                 numAlts++;
387                         }
388                 }
389                 if (numAlts < 1) {return VOID_VAL;}
390                 return totalAltitude / numAlts;
391         }
392
393         /**
394          * @return true if a thread is currently running
395          */
396         public boolean isRunning()
397         {
398                 return _running;
399         }
400
401         private void showConfirmMessage(int numDownloaded)
402         {
403                 if (numDownloaded == 1)
404                 {
405                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getTextWithNumber("confirm.downloadsrtm.1", numDownloaded),
406                                 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
407                 }
408                 else if (numDownloaded > 1)
409                 {
410                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getTextWithNumber("confirm.downloadsrtm", numDownloaded),
411                                 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
412                 }
413         }
414 }