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