1 package tim.prune.load;
3 import java.awt.event.ActionEvent;
4 import java.awt.event.ActionListener;
6 import java.util.TreeSet;
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;
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;
35 * Class to manage the loading of Jpegs and dealing with the GPS data from them
37 public class JpegLoader implements Runnable
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;
53 * @param inApp Application object to inform of photo load
54 * @param inParentFrame parent frame to reference for dialogs
56 public JpegLoader(App inApp, JFrame inParentFrame)
59 _parentFrame = inParentFrame;
64 * Open the GUI to select options and start the load
66 public void openDialog()
68 // TODO: Allow restriction of load area, either to current track or manually-entered range
69 if (_fileChooser == null)
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);
85 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
87 // Bring up dialog before starting
89 new Thread(this).start();
95 * Show the main dialog
97 private void showDialog()
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)
117 panel.add(cancelButton);
118 _progressDialog.getContentPane().add(panel);
119 _progressDialog.pack();
120 _progressDialog.show();
125 * Run method for performing tasks in separate thread
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);
140 // Process the files recursively and build lists of photos
141 processFileList(files, true, _subdirCheckbox.isSelected());
142 _progressDialog.hide();
143 if (_cancelled) {return;}
145 //System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1]
146 // + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
147 if (_fileCounts[0] == 0)
149 // No files found at all
150 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"),
151 I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
153 else if (_fileCounts[1] == 0)
156 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"),
157 I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
159 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
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);
165 else if (!_noExifCheckbox.isSelected() && _fileCounts[3] == 0)
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);
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);
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
187 private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
191 // Loop over elements in array
192 for (int i=0; i<inFiles.length; i++)
194 File file = inFiles[i];
195 if (file.exists() && file.canRead())
197 // Check whether it's a file or a directory
202 else if (file.isDirectory() && (inFirstDir || inDescend))
204 // Always process first directory,
205 // only process subdirectories if checkbox selected
206 File[] files = file.listFiles();
207 processFileList(files, false, inDescend);
212 // file doesn't exist or isn't readable - ignore error
214 // check for cancel button pressed
215 if (_cancelled) break;
222 * Process the given file, by attempting to extract its tags
223 * @param inFile file object to read
225 private void processFile(File inFile)
227 // Update progress bar
228 _fileCounts[0]++; // file found
229 _progressBar.setValue(_fileCounts[0]);
230 _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum());
231 _progressBar.repaint();
233 // Check whether filename corresponds with accepted filenames
234 if (!acceptPhotoFilename(inFile.getName())) {return;}
236 // Create Photo object
237 Photo photo = new Photo(inFile);
238 // Try to get information out of exif
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())
247 if (jpegData.getGpsDatestamp() != null && jpegData.getGpsTimestamp() != null)
249 photo.setTimestamp(createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp()));
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);
258 // Use exif timestamp if gps timestamp not available
259 if (photo.getTimestamp() == null && jpegData.getOriginalTimestamp() != null)
261 photo.setTimestamp(createTimestamp(jpegData.getOriginalTimestamp()));
263 photo.setExifThumbnail(jpegData.getThumbnailImage());
265 catch (JpegException jpe) { // don't list errors, just count them
267 // Use file timestamp if exif timestamp isn't available
268 if (photo.getTimestamp() == null)
270 photo.setTimestamp(new Timestamp(inFile.lastModified()));
271 //System.out.println("No exif, using timestamp from file: " + inFile.lastModified() + " -> " + photo.getTimestamp().getText());
275 //System.out.println("timestamp from file = " + photo.getTimestamp().getText());
277 // Add the photo if it's got a point or if pointless photos should be added
278 if (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
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
292 private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
297 // Loop over elements in array
298 for (int i=0; i<inFiles.length; i++)
300 File file = inFiles[i];
301 if (file.exists() && file.canRead())
303 // Check whether it's a file or a directory
308 else if (file.isDirectory() && (inFirstDir || inDescend))
310 fileCount += countFileList(file.listFiles(), false, inDescend);
320 * Create a DataPoint object from the given jpeg data
321 * @param inData Jpeg data including coordinates
322 * @return DataPoint object for Track
324 private static DataPoint createDataPoint(JpegData inData)
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)
336 altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES);
338 return new DataPoint(latitude, longitude, altitude);
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
348 private static double getCoordinateDoubleValue(Rational[] inRationals, boolean isPositive)
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;
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
367 private static Timestamp createTimestamp(Rational[] inDate, Rational[] inTime)
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());
377 * Use the given String value to create a timestamp
378 * @param inStamp timestamp from exif
379 * @return Timestamp object corresponding to input
381 private static Timestamp createTimestamp(String inStamp)
383 Timestamp stamp = null;
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)));
393 catch (NumberFormatException nfe) {}
399 * Check whether to accept the given filename
400 * @param inName name of file
401 * @return true if accepted, false otherwise
403 private static boolean acceptPhotoFilename(String inName)
405 if (inName != null && inName.length() > 4)
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"))
413 // If not found, check for file extension jpeg
414 if (inName.length() > 5)
416 String lastFive = inName.substring(inName.length() - 5).toLowerCase();
417 if (lastFive.equals(".jpeg"))
423 // Not matched so don't accept