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