File: LookupSrtmFunction.java

package info (click to toggle)
gpsprune 26.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,824 kB
  • sloc: java: 52,154; sh: 25; makefile: 21; python: 15
file content (406 lines) | stat: -rw-r--r-- 12,447 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
package tim.prune.function.srtm;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.swing.JOptionPane;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.GpsPrune;
import tim.prune.I18nManager;
import tim.prune.cmd.EditAltitudeCmd;
import tim.prune.config.Config;
import tim.prune.data.DataPoint;
import tim.prune.data.NumberUtils;
import tim.prune.data.Track;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.edit.PointAltitudeEdit;
import tim.prune.gui.ProgressDialog;
import tim.prune.tips.TipManager;

/**
 * Class to provide a lookup function for point altitudes using the Space
 * Shuttle's SRTM data files. HGT files are downloaded via HTTP and
 * point altitudes can then be interpolated from the grid data.
 */
public class LookupSrtmFunction extends GenericFunction
{
	/** Progress dialog */
	private ProgressDialog _progress = null;
	/** Track to process */
	private Track _track = null;
	/** Flag for whether this is a real track or a terrain one */
	private boolean _normalTrack = true;
	/** Flag set when any tiles had to be downloaded (but not cached) */
	private boolean _hadToDownload = false;
	/** Count the number of tiles downloaded and cached */
	private int _numCached = 0;
	/** Flag to check whether this function is currently running or not */
	private boolean _running = false;
	private boolean _cancelled = false;


	/**
	 * Constructor
	 * @param inApp  App object
	 */
	public LookupSrtmFunction(App inApp) {
		super(inApp);
	}

	/** @return name key */
	public String getNameKey() {
		return "function.lookupsrtm";
	}

	/**
	 * Begin the lookup using the normal track
	 */
	public void begin() {
		begin(_app.getTrackInfo().getTrack(), true);
	}

	/**
	 * Begin the lookup with an alternative track
	 * @param inAlternativeTrack
	 */
	public void begin(Track inAlternativeTrack) {
		begin(inAlternativeTrack, false);
	}

	/**
	 * Begin the function with the given parameters
	 * @param inTrack track to process
	 * @param inNormalTrack true if this is a "normal" track, false for an artificially constructed one such as for terrain
	 */
	private void begin(Track inTrack, boolean inNormalTrack)
	{
		_running = true;
		_cancelled = false;
		_hadToDownload = false;
		if (_progress == null) {
			_progress = new ProgressDialog(_parentFrame, getNameKey(), null, () -> _cancelled = true);
		}
		_progress.show();
		_track = inTrack;
		_normalTrack = inNormalTrack;
		// start new thread for time-consuming part
		new Thread(this::run).start();
	}

	/**
	 * Run method using separate thread
	 */
	public void run()
	{
		boolean hasZeroAltitudePoints = false;
		boolean hasNonZeroAltitudePoints = false;
		// First, loop to see what kind of points we have
		for (int i = 0; i < _track.getNumPoints(); i++)
		{
			if (_track.getPoint(i).hasAltitude())
			{
				if (_track.getPoint(i).getAltitude().getValue() == 0) {
					hasZeroAltitudePoints = true;
				}
				else {
					hasNonZeroAltitudePoints = true;
				}
			}
		}
		// Should we overwrite the zero altitude values?
		boolean overwriteZeros = hasZeroAltitudePoints && !hasNonZeroAltitudePoints;
		// If non-zero values present as well, ask user whether to overwrite the zeros or not
		if (hasNonZeroAltitudePoints && hasZeroAltitudePoints && JOptionPane.showConfirmDialog(_parentFrame,
			I18nManager.getText("dialog.lookupsrtm.overwritezeros"), getName(),
			JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
		{
			overwriteZeros = true;
		}

		// Now loop again to extract the required tiles
		HashSet<SrtmTile> tileSet = new HashSet<SrtmTile>();
		for (int i = 0; i < _track.getNumPoints(); i++)
		{
			// Consider points which don't have altitudes or have zero values
			if (!_track.getPoint(i).hasAltitude()
				|| (overwriteZeros && _track.getPoint(i).getAltitude().getValue() == 0))
			{
				tileSet.add(new SrtmTile(_track.getPoint(i)));
			}
		}

		CookieHandler regularCookieHandler = CookieHandler.getDefault();
		CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
		lookupValues(tileSet, overwriteZeros);
		CookieHandler.setDefault(regularCookieHandler);

		// Finished
		_running = false;
		// Show tip if lots of online lookups were necessary
		if (_hadToDownload) {
			_app.showTip(TipManager.Tip_DownloadSrtm);
		}
		else if (_numCached > 0) {
			showConfirmMessage(_numCached);
		}
	}


	/**
	 * Lookup the values from SRTM data
	 * @param inTileSet set of tiles to get
	 * @param inOverwriteZeros true to overwrite zero altitude values
	 */
	private void lookupValues(HashSet<SrtmTile> inTileSet, boolean inOverwriteZeros)
	{
		final String diskCachePath = getConfig().getConfigString(Config.KEY_DISK_CACHE);
		SrtmSource[] tileSources = new SrtmSource[] {
			new SrtmHighResSource(diskCachePath, getConfig().getConfigString(Config.KEY_EARTHDATA_AUTH)),
			new SrtmLowResSource(diskCachePath)};
		String errorMessage = null;
		final int numTiles = inTileSet.size();

		// Update progress bar
		if (_progress != null) {
			_progress.showProgress(0, numTiles);
		}
		int currentTileIndex = 0;
		_numCached = 0;
		ArrayList<PointAltitudeEdit> edits = new ArrayList<PointAltitudeEdit>();
		for (SrtmTile tile : inTileSet)
		{
			// Set progress
			_progress.showProgress(currentTileIndex++, numTiles);
			int[] heights = null;
			for (SrtmSource tileSource : tileSources)
			{
				if (heights == null && !_cancelled)
				{
					try
					{
						heights = getHeightsForTile(tile, tileSource);
						edits.addAll(applySrtmTileToWholeTrack(tile, heights, inOverwriteZeros,
							tileSource.getTilePixels()));
					}
					catch (IOException ioe) {
						errorMessage = ioe.getClass().getName() + " - " + ioe.getMessage();
					} catch (SrtmAuthException authExc) {
						errorMessage = I18nManager.getText("error.srtm.authenticationfailed") + " - " + authExc.getMessage();
					}
				}
			}
		}

		_progress.close();
		if (_cancelled) {
			return;
		}

		if (errorMessage != null) {
			_app.showErrorMessageNoLookup(getNameKey(), errorMessage);
		}
		if (!edits.isEmpty())
		{
			EditAltitudeCmd command = new EditAltitudeCmd(edits);
			// Apply this command according to whether it's a real track or not
			if (_normalTrack)
			{
				command.setConfirmText(I18nManager.getTextWithNumber("confirm.lookupsrtm", edits.size()));
				command.setDescription(getName());
				_app.execute(command);
			}
			else {
				command.executeCommand(_track);
			}
		}
		else if (numTiles > 0) {
			_app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonefound");
		}
		else {
			_app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonerequired");
		}
	}

	/**
	 * Get the height array for the given tile, using the given source
	 * @param inTile tile to get data for
	 * @param inTileSource tile source to use
	 * @return int array containing heights
	 * @throws IOException on IO failure
	 * @throws SrtmAuthException on authentication failure
	 */
	private int[] getHeightsForTile(SrtmTile inTile, SrtmSource inTileSource)
		throws IOException, SrtmAuthException
	{
		int[] heights = null;
		// Open zipinputstream on url and check size
		ZipInputStream inStream = getStreamToSrtmData(inTile, inTileSource);
		boolean entryOk = false;
		if (inStream != null)
		{
			ZipEntry entry = inStream.getNextEntry();
			entryOk = (entry != null && entry.getSize() == inTileSource.getTileSizeBytes());
			if (entryOk)
			{
				final int ARRLENGTH = inTileSource.getTilePixels() * inTileSource.getTilePixels();
				heights = new int[ARRLENGTH];

				// Read entire file contents into one byte array
				for (int i = 0; i < ARRLENGTH; i++)
				{
					heights[i] = inStream.read() * 256 + inStream.read();
					if (heights[i] >= 32768) {heights[i] -= 65536;}
				}
			}
			// Close stream from url
			inStream.close();
		}

		if (!entryOk) {
			heights = null;
		}
		return heights;
	}

	/**
	 * See whether the SRTM file is already available locally first, then try online
	 * @param inTile tile to get
	 * @param inSrtmSource source of data to use
	 * @return ZipInputStream either on the local file or on the downloaded zip file
	 * @throws SrtmAuthException if authentication failed
	 */
	private ZipInputStream getStreamToSrtmData(SrtmTile inTile, SrtmSource inSrtmSource)
	throws IOException, SrtmAuthException
	{
		ZipInputStream localData = null;
		try {
			localData = getStreamToLocalHgtFile(inSrtmSource.getCacheDir(),
				inSrtmSource.getFilename(inTile));
		}
		catch (IOException ioe) {
			localData = null;
		}
		if (localData != null)
		{
			return localData;
		}
		// try to download to cache
		SrtmSource.Result result = inSrtmSource.downloadTile(inTile);
		if (result == SrtmSource.Result.DOWNLOADED)
		{
			_numCached++;
			return getStreamToLocalHgtFile(inSrtmSource.getCacheDir(), inSrtmSource.getFilename(inTile));
		}
		if (result == SrtmSource.Result.NOT_ENABLED) {
			return null;
		}
		// If we don't have a cache, we may be able to download it temporarily
		if (result != SrtmSource.Result.DOWNLOAD_FAILED)
		{
			_hadToDownload = true;
			URL tileUrl = inSrtmSource.getUrl(inTile);
			if (tileUrl == null) {
				return null;
			}
			URLConnection conn = tileUrl.openConnection();
			conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER);
			return new ZipInputStream(conn.getInputStream());
		}
		// everything failed
		return null;
	}

	/**
	 * Get the SRTM file from the local cache, if available
	 * @param inFilename filename to look for
	 * @return ZipInputStream on the local file or null if not there
	 */
	private ZipInputStream getStreamToLocalHgtFile(File inCacheDir, String inFilename)
	throws IOException
	{
		if (inCacheDir != null && inCacheDir.exists()
			&& inCacheDir.isDirectory() && inCacheDir.canRead())
		{
			File srtmFile = new File(inCacheDir, inFilename);
			if (srtmFile.exists() && srtmFile.isFile() && srtmFile.canRead()
				&& srtmFile.length() > 400)
			{
				// File found, use this one
				return new ZipInputStream(new FileInputStream(srtmFile));
			}
		}
		return null;
	}

	/**
	 * Given the height data read in from file, generate the edits to modify the track
	 * @param inTile tile being applied
	 * @param inHeights height data read in from file
	 * @param inOverwriteZeros true to overwrite zero altitude values
	 * @param inTilePixelsPerSide number of pixels on side of tile
	 * @return list of edits to apply
	 */
	private ArrayList<PointAltitudeEdit> applySrtmTileToWholeTrack(SrtmTile inTile, int[] inHeights,
		boolean inOverwriteZeros, int inTilePixelsPerSide)
	{
		ArrayList<PointAltitudeEdit> edits = new ArrayList<>();
		if (inHeights == null) {
			return edits;
		}
		// Loop over all points in track, try to apply altitude from array
		for (int p = 0; p < _track.getNumPoints(); p++)
		{
			DataPoint point = _track.getPoint(p);
			final boolean doCalculation = !point.hasAltitude()
				|| (inOverwriteZeros && point.getAltitude().getValue() == 0);
			if (doCalculation && inTile.contains(point))
			{
				final double altitude = Interpolator.calculateAltitude(point.getLongitude().getDouble(),
					point.getLatitude().getDouble(), inHeights, _normalTrack, inTilePixelsPerSide);
				if (altitude != SrtmSource.VOID_VAL)
				{
					// Found an altitude, so create a command for it
					// (use UK Locale to force a decimal point when rounding the decimal value
					// instead of using the locale-specific character like comma)
					String roundedValue = NumberUtils.formatNumberUk(altitude, 3);
					edits.add(new PointAltitudeEdit(p, roundedValue, UnitSetLibrary.UNITS_METRES));
				}
			}
		}
		return edits;
	}

	/**
	 * @return true if a thread is currently running
	 */
	public boolean isRunning() {
		return _running;
	}

	private void showConfirmMessage(int numDownloaded)
	{
		if (numDownloaded == 1)
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getTextWithNumber("confirm.downloadsrtm.1", numDownloaded),
				getName(), JOptionPane.INFORMATION_MESSAGE);
		}
		else if (numDownloaded > 1)
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getTextWithNumber("confirm.downloadsrtm", numDownloaded),
				getName(), JOptionPane.INFORMATION_MESSAGE);
		}
	}
}