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.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;
37 * Class to manage the loading of Jpegs and dealing with the GPS data from them
39 public class JpegLoader implements Runnable
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;
58 * @param inApp Application object to inform of photo load
59 * @param inParentFrame parent frame to reference for dialogs
61 public JpegLoader(App inApp, JFrame inParentFrame)
64 _parentFrame = inParentFrame;
65 String[] fileTypes = {"jpg", "jpe", "jpeg"};
66 _fileFilter = new GenericFileFilter("filetype.jpeg", fileTypes);
71 * Open the GUI to select options and start the load
72 * @param inRectangle track rectangle
74 public void openDialog(LatLonRectangle inRectangle)
76 // Create file chooser if necessary
77 if (_fileChooser == null)
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);}
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)
106 // Bring up dialog before starting
108 new Thread(this).start();
114 * Show the main dialog
116 private void showDialog()
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)
136 panel.add(cancelButton);
137 _progressDialog.getContentPane().add(panel);
138 _progressDialog.pack();
139 _progressDialog.show();
144 * Run method for performing tasks in separate thread
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);
159 // Process the files recursively and build lists of photos
160 processFileList(files, true, _subdirCheckbox.isSelected());
161 _progressDialog.hide();
162 if (_cancelled) {return;}
164 //System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1]
165 // + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
166 if (_fileCounts[0] == 0)
168 // No files found at all
169 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"),
170 I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
172 else if (_fileCounts[1] == 0)
175 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"),
176 I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
178 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
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);
184 else if (!_noExifCheckbox.isSelected() && _fileCounts[3] == 0)
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);
192 // Found some photos to load - pass information back to app
193 _app.informPhotosLoaded(_photos);
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
204 private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
208 // Loop over elements in array
209 for (int i=0; i<inFiles.length; i++)
211 File file = inFiles[i];
212 if (file.exists() && file.canRead())
214 // Check whether it's a file or a directory
219 else if (file.isDirectory() && (inFirstDir || inDescend))
221 // Always process first directory,
222 // only process subdirectories if checkbox selected
223 File[] files = file.listFiles();
224 processFileList(files, false, inDescend);
229 // file doesn't exist or isn't readable - ignore error
231 // check for cancel button pressed
232 if (_cancelled) break;
239 * Process the given file, by attempting to extract its tags
240 * @param inFile file object to read
242 private void processFile(File inFile)
244 // Update progress bar
245 _fileCounts[0]++; // file found
246 _progressBar.setValue(_fileCounts[0]);
247 _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum());
248 _progressBar.repaint();
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)
254 // Create Photo object
255 Photo photo = new Photo(inFile);
256 // Try to get information out of exif
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())
265 if (jpegData.getGpsDatestamp() != null && jpegData.getGpsTimestamp() != null)
267 photo.setTimestamp(createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp()));
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);
277 // Use exif timestamp if gps timestamp not available
278 if (photo.getTimestamp() == null && jpegData.getOriginalTimestamp() != null)
280 photo.setTimestamp(createTimestamp(jpegData.getOriginalTimestamp()));
282 photo.setExifThumbnail(jpegData.getThumbnailImage());
284 catch (JpegException jpe) { // don't list errors, just count them
286 // Use file timestamp if exif timestamp isn't available
287 if (photo.getTimestamp() == null) {
288 photo.setTimestamp(new Timestamp(inFile.lastModified()));
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())))
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
307 private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
312 // Loop over elements in array
313 for (int i=0; i<inFiles.length; i++)
315 File file = inFiles[i];
316 if (file.exists() && file.canRead())
318 // Store first directory in config for later
319 if (i == 0 && inFirstDir) {
320 Config.setWorkingDirectory(file.isDirectory()?file:file.getParentFile());
322 // Check whether it's a file or a directory
327 else if (file.isDirectory() && (inFirstDir || inDescend))
329 fileCount += countFileList(file.listFiles(), false, inDescend);
339 * Create a DataPoint object from the given jpeg data
340 * @param inData Jpeg data including coordinates
341 * @return DataPoint object for Track
343 private static DataPoint createDataPoint(JpegData inData)
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)
355 altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES);
357 return new DataPoint(latitude, longitude, altitude);
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
367 private static double getCoordinateDoubleValue(Rational[] inRationals, boolean isPositive)
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;
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
386 private static Timestamp createTimestamp(Rational[] inDate, Rational[] inTime)
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());
396 * Use the given String value to create a timestamp
397 * @param inStamp timestamp from exif
398 * @return Timestamp object corresponding to input
400 private static Timestamp createTimestamp(String inStamp)
402 Timestamp stamp = null;
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)));
412 catch (NumberFormatException nfe) {}