File: JpegLoader.java

package info (click to toggle)
gpsprune 17-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,984 kB
  • ctags: 5,218
  • sloc: java: 39,403; sh: 25; makefile: 17; python: 15
file content (383 lines) | stat: -rw-r--r-- 12,581 bytes parent folder | download | duplicates (3)
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
package tim.prune.load;

import java.io.File;
import java.util.TreeSet;

import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;

import tim.prune.App;
import tim.prune.I18nManager;
import tim.prune.config.Config;
import tim.prune.data.Altitude;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.LatLonRectangle;
import tim.prune.data.Latitude;
import tim.prune.data.Longitude;
import tim.prune.data.Photo;
import tim.prune.data.Timestamp;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.Cancellable;
import tim.prune.jpeg.ExifGateway;
import tim.prune.jpeg.JpegData;

/**
 * Class to manage the loading of Jpegs and dealing with the GPS data from them
 */
public class JpegLoader implements Runnable, Cancellable
{
	private App _app = null;
	private JFrame _parentFrame = null;
	private JFileChooser _fileChooser = null;
	private GenericFileFilter _fileFilter = null;
	private JCheckBox _subdirCheckbox = null;
	private JCheckBox _noExifCheckbox = null;
	private JCheckBox _outsideAreaCheckbox = null;
	private MediaLoadProgressDialog _progressDialog = null;
	private int[] _fileCounts = null;
	private boolean _cancelled = false;
	private LatLonRectangle _trackRectangle = null;
	private TreeSet<Photo> _photos = null;


	/**
	 * Constructor
	 * @param inApp Application object to inform of photo load
	 * @param inParentFrame parent frame to reference for dialogs
	 */
	public JpegLoader(App inApp, JFrame inParentFrame)
	{
		_app = inApp;
		_parentFrame = inParentFrame;
		_fileFilter = new JpegFileFilter();
	}


	/**
	 * Open the GUI to select options and start the load
	 * @param inRectangle track rectangle
	 */
	public void openDialog(LatLonRectangle inRectangle)
	{
		// Create file chooser if necessary
		if (_fileChooser == null)
		{
			_fileChooser = new JFileChooser();
			_fileChooser.setMultiSelectionEnabled(true);
			_fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
			_fileChooser.setFileFilter(_fileFilter);
			_fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
			_subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
			_subdirCheckbox.setSelected(true);
			_noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
			_noExifCheckbox.setSelected(true);
			_outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
			_outsideAreaCheckbox.setSelected(true);
			JPanel panel = new JPanel();
			panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
			panel.add(_subdirCheckbox);
			panel.add(_noExifCheckbox);
			panel.add(_outsideAreaCheckbox);
			_fileChooser.setAccessory(panel);
			// start from directory in config if already set by other operations
			String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
			if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
			if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
		}
		// enable/disable track checkbox
		_trackRectangle = inRectangle;
		_outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
		// Show file dialog to choose file / directory(ies)
		if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
		{
			// Bring up dialog before starting
			_progressDialog = new MediaLoadProgressDialog(_parentFrame, this);
			_progressDialog.show();
			// start thread for processing
			new Thread(this).start();
		}
	}

	/** Cancel */
	public void cancel() {
		_cancelled = true;
	}


	/**
	 * Run method for performing tasks in separate thread
	 */
	public void run()
	{
		// Initialise arrays, errors, summaries
		_fileCounts = new int[3]; // files, jpegs, gps
		_photos = new TreeSet<Photo>(new MediaSorter());
		File[] files = _fileChooser.getSelectedFiles();
		// Loop recursively over selected files/directories to count files
		int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
		// Set up the progress bar for this number of files
		_progressDialog.showProgress(0, numFiles);
		_cancelled = false;

		// Process the files recursively and build lists of photos
		processFileList(files, true, _subdirCheckbox.isSelected());
		_progressDialog.close();
		if (_cancelled) {return;}

		if (_fileCounts[0] == 0)
		{
			// No files found at all
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
		}
		else if (_fileCounts[1] == 0)
		{
			// No jpegs found
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
		}
		else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
		{
			// Need coordinates but no gps information found
			_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
		}
		else
		{
			// Found some photos to load - pass information back to app
			_app.informPhotosLoaded(_photos);
		}
	}


	/**
	 * Process a list of files and/or directories
	 * @param inFiles array of file/directories
	 * @param inFirstDir true if first directory
	 * @param inDescend true to descend to subdirectories
	 */
	private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
	{
		if (inFiles == null) return;
		// Loop over elements in array
		for (int i=0; i<inFiles.length && !_cancelled; i++)
		{
			File file = inFiles[i];
			if (file.exists() && file.canRead())
			{
				// Check whether it's a file or a directory
				if (file.isFile())
				{
					processFile(file);
				}
				else if (file.isDirectory() && (inFirstDir || inDescend))
				{
					// Always process first directory,
					// only process subdirectories if checkbox selected
					File[] files = file.listFiles();
					processFileList(files, false, inDescend);
				}
			}
			// if file doesn't exist or isn't readable - ignore
		}
	}


	/**
	 * Process the given file, by attempting to extract its tags
	 * @param inFile file object to read
	 */
	private void processFile(File inFile)
	{
		// Update progress bar
		_fileCounts[0]++; // file found
		_progressDialog.showProgress(_fileCounts[0], -1);

		// Check whether filename corresponds with accepted filenames
		if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
		// If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)

		if (inFile.exists() && inFile.canRead()) {
			_fileCounts[1]++; // jpeg found
		}
		Photo photo = createPhoto(inFile);
		if (photo.getDataPoint() != null) {
			_fileCounts[2]++; // photo has coordinates
		}
		// Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
		if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
			&& (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
				|| _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
		{
			_photos.add(photo);
		}
	}

	/**
	 * Create a Photo object for the given file, including reading exif information
	 * @param inFile file object
	 * @return Photo object
	 */
	public static Photo createPhoto(File inFile)
	{
		// Create Photo object
		Photo photo = new Photo(inFile);
		// Try to get information out of exif
		JpegData jpegData = ExifGateway.getJpegData(inFile);
		Timestamp timestamp = null;
		if (jpegData != null)
		{
			if (jpegData.isGpsValid())
			{
				timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
				// Make DataPoint and attach to Photo
				DataPoint point = createDataPoint(jpegData);
				point.setPhoto(photo);
				point.setSegmentStart(true);
				photo.setDataPoint(point);
				photo.setOriginalStatus(Photo.Status.TAGGED);
			}
			// Use exif timestamp if gps timestamp not available
			if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
				timestamp = createTimestamp(jpegData.getOriginalTimestamp());
			}
			if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
				timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
			}
			photo.setExifThumbnail(jpegData.getThumbnailImage());
			// Also extract orientation tag for setting rotation state of photo
			photo.setRotation(jpegData.getRequiredRotation());
			// Set bearing, if any
			photo.setBearing(jpegData.getBearing());
		}
		// Use file timestamp if exif timestamp isn't available
		if (timestamp == null) {
			timestamp = new Timestamp(inFile.lastModified());
		}
		// Apply timestamp to photo and its point (if any)
		photo.setTimestamp(timestamp);
		if (photo.getDataPoint() != null) {
			photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.Format.ISO8601), false);
		}
		return photo;
	}


	/**
	 * Recursively count the selected Files so we can draw a progress bar
	 * @param inFiles file list
	 * @param inFirstDir true if first directory
	 * @param inDescend true to descend to subdirectories
	 * @return count of the files selected
	 */
	private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
	{
		int fileCount = 0;
		if (inFiles != null)
		{
			// Loop over elements in array
			for (int i=0; i<inFiles.length; i++)
			{
				File file = inFiles[i];
				if (file.exists() && file.canRead())
				{
					// Store first directory in config for later
					if (i == 0 && inFirstDir) {
						File workingDir = file.isDirectory()?file:file.getParentFile();
						Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
					}
					// Check whether it's a file or a directory
					if (file.isFile())
					{
						fileCount++;
					}
					else if (file.isDirectory() && (inFirstDir || inDescend))
					{
						fileCount += countFileList(file.listFiles(), false, inDescend);
					}
				}
			}
		}
		return fileCount;
	}


	/**
	 * Create a DataPoint object from the given jpeg data
	 * @param inData Jpeg data including coordinates
	 * @return DataPoint object for Track
	 */
	private static DataPoint createDataPoint(JpegData inData)
	{
		// Create model objects from jpeg data
		double latval = getCoordinateDoubleValue(inData.getLatitude(),
			inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
		Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
		double lonval = getCoordinateDoubleValue(inData.getLongitude(),
			inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
		Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
		Altitude altitude = null;
		if (inData.hasAltitude()) {
			altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
		}
		return new DataPoint(latitude, longitude, altitude);
	}


	/**
	 * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
	 * @param inValues array of three doubles for deg-min-sec
	 * @param isPositive true for positive hemisphere, for positive double value
	 * @return double value of coordinate, either positive or negative
	 */
	private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
	{
		if (inValues == null || inValues.length != 3) return 0.0;
		double value = inValues[0]        // degrees
			+ inValues[1] / 60.0          // minutes
			+ inValues[2] / 60.0 / 60.0;  // seconds
		// make sure it's the correct sign
		value = Math.abs(value);
		if (!isPositive) value = -value;
		return value;
	}


	/**
	 * Use the given int values to create a timestamp
	 * @param inDate ints describing date
	 * @param inTime ints describing time
	 * @return Timestamp object corresponding to inputs
	 */
	private static Timestamp createTimestamp(int[] inDate, int[] inTime)
	{
		if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
			return null;
		}
		return new Timestamp(inDate[0], inDate[1], inDate[2],
			inTime[0], inTime[1], inTime[2]);
	}


	/**
	 * Use the given String value to create a timestamp
	 * @param inStamp timestamp from exif
	 * @return Timestamp object corresponding to input
	 */
	private static Timestamp createTimestamp(String inStamp)
	{
		Timestamp stamp = null;
		try
		{
			stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
				Integer.parseInt(inStamp.substring(5, 7)),
				Integer.parseInt(inStamp.substring(8, 10)),
				Integer.parseInt(inStamp.substring(11, 13)),
				Integer.parseInt(inStamp.substring(14, 16)),
				Integer.parseInt(inStamp.substring(17)));
		}
		catch (NumberFormatException nfe) {}
		return stamp;
	}
}