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