]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/load/JpegLoader.java
Version 17, September 2014
[GpsPrune.git] / tim / prune / load / JpegLoader.java
1 package tim.prune.load;
2
3 import java.io.File;
4 import java.util.TreeSet;
5
6 import javax.swing.BoxLayout;
7 import javax.swing.JCheckBox;
8 import javax.swing.JFileChooser;
9 import javax.swing.JFrame;
10 import javax.swing.JPanel;
11
12 import tim.prune.App;
13 import tim.prune.I18nManager;
14 import tim.prune.config.Config;
15 import tim.prune.data.Altitude;
16 import tim.prune.data.DataPoint;
17 import tim.prune.data.Field;
18 import tim.prune.data.LatLonRectangle;
19 import tim.prune.data.Latitude;
20 import tim.prune.data.Longitude;
21 import tim.prune.data.Photo;
22 import tim.prune.data.Timestamp;
23 import tim.prune.data.UnitSetLibrary;
24 import tim.prune.function.Cancellable;
25 import tim.prune.jpeg.ExifGateway;
26 import tim.prune.jpeg.JpegData;
27
28 /**
29  * Class to manage the loading of Jpegs and dealing with the GPS data from them
30  */
31 public class JpegLoader implements Runnable, Cancellable
32 {
33         private App _app = null;
34         private JFrame _parentFrame = null;
35         private JFileChooser _fileChooser = null;
36         private GenericFileFilter _fileFilter = null;
37         private JCheckBox _subdirCheckbox = null;
38         private JCheckBox _noExifCheckbox = null;
39         private JCheckBox _outsideAreaCheckbox = null;
40         private MediaLoadProgressDialog _progressDialog = null;
41         private int[] _fileCounts = null;
42         private boolean _cancelled = false;
43         private LatLonRectangle _trackRectangle = null;
44         private TreeSet<Photo> _photos = null;
45
46
47         /**
48          * Constructor
49          * @param inApp Application object to inform of photo load
50          * @param inParentFrame parent frame to reference for dialogs
51          */
52         public JpegLoader(App inApp, JFrame inParentFrame)
53         {
54                 _app = inApp;
55                 _parentFrame = inParentFrame;
56                 _fileFilter = new JpegFileFilter();
57         }
58
59
60         /**
61          * Open the GUI to select options and start the load
62          * @param inRectangle track rectangle
63          */
64         public void openDialog(LatLonRectangle inRectangle)
65         {
66                 // Create file chooser if necessary
67                 if (_fileChooser == null)
68                 {
69                         _fileChooser = new JFileChooser();
70                         _fileChooser.setMultiSelectionEnabled(true);
71                         _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
72                         _fileChooser.setFileFilter(_fileFilter);
73                         _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
74                         _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
75                         _subdirCheckbox.setSelected(true);
76                         _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
77                         _noExifCheckbox.setSelected(true);
78                         _outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
79                         _outsideAreaCheckbox.setSelected(true);
80                         JPanel panel = new JPanel();
81                         panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
82                         panel.add(_subdirCheckbox);
83                         panel.add(_noExifCheckbox);
84                         panel.add(_outsideAreaCheckbox);
85                         _fileChooser.setAccessory(panel);
86                         // start from directory in config if already set by other operations
87                         String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
88                         if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
89                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
90                 }
91                 // enable/disable track checkbox
92                 _trackRectangle = inRectangle;
93                 _outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
94                 // Show file dialog to choose file / directory(ies)
95                 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
96                 {
97                         // Bring up dialog before starting
98                         _progressDialog = new MediaLoadProgressDialog(_parentFrame, this);
99                         _progressDialog.show();
100                         // start thread for processing
101                         new Thread(this).start();
102                 }
103         }
104
105         /** Cancel */
106         public void cancel() {
107                 _cancelled = true;
108         }
109
110
111         /**
112          * Run method for performing tasks in separate thread
113          */
114         public void run()
115         {
116                 // Initialise arrays, errors, summaries
117                 _fileCounts = new int[3]; // files, jpegs, gps
118                 _photos = new TreeSet<Photo>(new MediaSorter());
119                 File[] files = _fileChooser.getSelectedFiles();
120                 // Loop recursively over selected files/directories to count files
121                 int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
122                 // Set up the progress bar for this number of files
123                 _progressDialog.showProgress(0, numFiles);
124                 _cancelled = false;
125
126                 // Process the files recursively and build lists of photos
127                 processFileList(files, true, _subdirCheckbox.isSelected());
128                 _progressDialog.close();
129                 if (_cancelled) {return;}
130
131                 if (_fileCounts[0] == 0)
132                 {
133                         // No files found at all
134                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
135                 }
136                 else if (_fileCounts[1] == 0)
137                 {
138                         // No jpegs found
139                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
140                 }
141                 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
142                 {
143                         // Need coordinates but no gps information found
144                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
145                 }
146                 else
147                 {
148                         // Found some photos to load - pass information back to app
149                         _app.informPhotosLoaded(_photos);
150                 }
151         }
152
153
154         /**
155          * Process a list of files and/or directories
156          * @param inFiles array of file/directories
157          * @param inFirstDir true if first directory
158          * @param inDescend true to descend to subdirectories
159          */
160         private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
161         {
162                 if (inFiles == null) return;
163                 // Loop over elements in array
164                 for (int i=0; i<inFiles.length && !_cancelled; i++)
165                 {
166                         File file = inFiles[i];
167                         if (file.exists() && file.canRead())
168                         {
169                                 // Check whether it's a file or a directory
170                                 if (file.isFile())
171                                 {
172                                         processFile(file);
173                                 }
174                                 else if (file.isDirectory() && (inFirstDir || inDescend))
175                                 {
176                                         // Always process first directory,
177                                         // only process subdirectories if checkbox selected
178                                         File[] files = file.listFiles();
179                                         processFileList(files, false, inDescend);
180                                 }
181                         }
182                         // if file doesn't exist or isn't readable - ignore
183                 }
184         }
185
186
187         /**
188          * Process the given file, by attempting to extract its tags
189          * @param inFile file object to read
190          */
191         private void processFile(File inFile)
192         {
193                 // Update progress bar
194                 _fileCounts[0]++; // file found
195                 _progressDialog.showProgress(_fileCounts[0], -1);
196
197                 // Check whether filename corresponds with accepted filenames
198                 if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
199                 // If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
200
201                 if (inFile.exists() && inFile.canRead()) {
202                         _fileCounts[1]++; // jpeg found
203                 }
204                 Photo photo = createPhoto(inFile);
205                 if (photo.getDataPoint() != null) {
206                         _fileCounts[2]++; // photo has coordinates
207                 }
208                 // Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
209                 if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
210                         && (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
211                                 || _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
212                 {
213                         _photos.add(photo);
214                 }
215         }
216
217         /**
218          * Create a Photo object for the given file, including reading exif information
219          * @param inFile file object
220          * @return Photo object
221          */
222         public static Photo createPhoto(File inFile)
223         {
224                 // Create Photo object
225                 Photo photo = new Photo(inFile);
226                 // Try to get information out of exif
227                 JpegData jpegData = ExifGateway.getJpegData(inFile);
228                 Timestamp timestamp = null;
229                 if (jpegData != null)
230                 {
231                         if (jpegData.isGpsValid())
232                         {
233                                 timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
234                                 // Make DataPoint and attach to Photo
235                                 DataPoint point = createDataPoint(jpegData);
236                                 point.setPhoto(photo);
237                                 point.setSegmentStart(true);
238                                 photo.setDataPoint(point);
239                                 photo.setOriginalStatus(Photo.Status.TAGGED);
240                         }
241                         // Use exif timestamp if gps timestamp not available
242                         if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
243                                 timestamp = createTimestamp(jpegData.getOriginalTimestamp());
244                         }
245                         if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
246                                 timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
247                         }
248                         photo.setExifThumbnail(jpegData.getThumbnailImage());
249                         // Also extract orientation tag for setting rotation state of photo
250                         photo.setRotation(jpegData.getRequiredRotation());
251                         // Set bearing, if any
252                         photo.setBearing(jpegData.getBearing());
253                 }
254                 // Use file timestamp if exif timestamp isn't available
255                 if (timestamp == null) {
256                         timestamp = new Timestamp(inFile.lastModified());
257                 }
258                 // Apply timestamp to photo and its point (if any)
259                 photo.setTimestamp(timestamp);
260                 if (photo.getDataPoint() != null) {
261                         photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.Format.ISO8601), false);
262                 }
263                 return photo;
264         }
265
266
267         /**
268          * Recursively count the selected Files so we can draw a progress bar
269          * @param inFiles file list
270          * @param inFirstDir true if first directory
271          * @param inDescend true to descend to subdirectories
272          * @return count of the files selected
273          */
274         private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
275         {
276                 int fileCount = 0;
277                 if (inFiles != null)
278                 {
279                         // Loop over elements in array
280                         for (int i=0; i<inFiles.length; i++)
281                         {
282                                 File file = inFiles[i];
283                                 if (file.exists() && file.canRead())
284                                 {
285                                         // Store first directory in config for later
286                                         if (i == 0 && inFirstDir) {
287                                                 File workingDir = file.isDirectory()?file:file.getParentFile();
288                                                 Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
289                                         }
290                                         // Check whether it's a file or a directory
291                                         if (file.isFile())
292                                         {
293                                                 fileCount++;
294                                         }
295                                         else if (file.isDirectory() && (inFirstDir || inDescend))
296                                         {
297                                                 fileCount += countFileList(file.listFiles(), false, inDescend);
298                                         }
299                                 }
300                         }
301                 }
302                 return fileCount;
303         }
304
305
306         /**
307          * Create a DataPoint object from the given jpeg data
308          * @param inData Jpeg data including coordinates
309          * @return DataPoint object for Track
310          */
311         private static DataPoint createDataPoint(JpegData inData)
312         {
313                 // Create model objects from jpeg data
314                 double latval = getCoordinateDoubleValue(inData.getLatitude(),
315                         inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
316                 Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
317                 double lonval = getCoordinateDoubleValue(inData.getLongitude(),
318                         inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
319                 Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
320                 Altitude altitude = null;
321                 if (inData.hasAltitude()) {
322                         altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
323                 }
324                 return new DataPoint(latitude, longitude, altitude);
325         }
326
327
328         /**
329          * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
330          * @param inValues array of three doubles for deg-min-sec
331          * @param isPositive true for positive hemisphere, for positive double value
332          * @return double value of coordinate, either positive or negative
333          */
334         private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
335         {
336                 if (inValues == null || inValues.length != 3) return 0.0;
337                 double value = inValues[0]        // degrees
338                         + inValues[1] / 60.0          // minutes
339                         + inValues[2] / 60.0 / 60.0;  // seconds
340                 // make sure it's the correct sign
341                 value = Math.abs(value);
342                 if (!isPositive) value = -value;
343                 return value;
344         }
345
346
347         /**
348          * Use the given int values to create a timestamp
349          * @param inDate ints describing date
350          * @param inTime ints describing time
351          * @return Timestamp object corresponding to inputs
352          */
353         private static Timestamp createTimestamp(int[] inDate, int[] inTime)
354         {
355                 if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
356                         return null;
357                 }
358                 return new Timestamp(inDate[0], inDate[1], inDate[2],
359                         inTime[0], inTime[1], inTime[2]);
360         }
361
362
363         /**
364          * Use the given String value to create a timestamp
365          * @param inStamp timestamp from exif
366          * @return Timestamp object corresponding to input
367          */
368         private static Timestamp createTimestamp(String inStamp)
369         {
370                 Timestamp stamp = null;
371                 try
372                 {
373                         stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
374                                 Integer.parseInt(inStamp.substring(5, 7)),
375                                 Integer.parseInt(inStamp.substring(8, 10)),
376                                 Integer.parseInt(inStamp.substring(11, 13)),
377                                 Integer.parseInt(inStamp.substring(14, 16)),
378                                 Integer.parseInt(inStamp.substring(17)));
379                 }
380                 catch (NumberFormatException nfe) {}
381                 return stamp;
382         }
383 }