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