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.JPanel;
17 import javax.swing.JProgressBar;
20 import tim.prune.I18nManager;
21 import tim.prune.config.Config;
22 import tim.prune.data.Altitude;
23 import tim.prune.data.DataPoint;
24 import tim.prune.data.Field;
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.Timestamp;
30 import tim.prune.jpeg.ExifGateway;
31 import tim.prune.jpeg.JpegData;
34 * Class to manage the loading of Jpegs and dealing with the GPS data from them
36 public class JpegLoader implements Runnable
38 private App _app = null;
39 private JFrame _parentFrame = null;
40 private JFileChooser _fileChooser = null;
41 private GenericFileFilter _fileFilter = null;
42 private JCheckBox _subdirCheckbox = null;
43 private JCheckBox _noExifCheckbox = null;
44 private JCheckBox _outsideAreaCheckbox = null;
45 private JDialog _progressDialog = null;
46 private JProgressBar _progressBar = null;
47 private int[] _fileCounts = null;
48 private boolean _cancelled = false;
49 private LatLonRectangle _trackRectangle = null;
50 private TreeSet<Photo> _photos = null;
55 * @param inApp Application object to inform of photo load
56 * @param inParentFrame parent frame to reference for dialogs
58 public JpegLoader(App inApp, JFrame inParentFrame)
61 _parentFrame = inParentFrame;
62 _fileFilter = new JpegFileFilter();
67 * Open the GUI to select options and start the load
68 * @param inRectangle track rectangle
70 public void openDialog(LatLonRectangle inRectangle)
72 // Create file chooser if necessary
73 if (_fileChooser == null)
75 _fileChooser = new JFileChooser();
76 _fileChooser.setMultiSelectionEnabled(true);
77 _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
78 _fileChooser.setFileFilter(_fileFilter);
79 _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
80 _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
81 _subdirCheckbox.setSelected(true);
82 _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
83 _noExifCheckbox.setSelected(true);
84 _outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
85 _outsideAreaCheckbox.setSelected(true);
86 JPanel panel = new JPanel();
87 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
88 panel.add(_subdirCheckbox);
89 panel.add(_noExifCheckbox);
90 panel.add(_outsideAreaCheckbox);
91 _fileChooser.setAccessory(panel);
92 // start from directory in config if already set by other operations
93 String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
94 if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
95 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
97 // enable/disable track checkbox
98 _trackRectangle = inRectangle;
99 _outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
100 // Show file dialog to choose file / directory(ies)
101 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
103 // Bring up dialog before starting
104 if (_progressDialog == null) {
105 createProgressDialog();
107 // reset dialog and show it
108 _progressBar.setValue(0);
109 _progressBar.setString("");
110 _progressDialog.setVisible(true);
111 // start thread for processing
112 new Thread(this).start();
118 * Create the dialog to show the progress
120 private void createProgressDialog()
122 _progressDialog = new JDialog(_parentFrame, I18nManager.getText("dialog.jpegload.progress.title"));
123 _progressDialog.setLocationRelativeTo(_parentFrame);
124 _progressBar = new JProgressBar(0, 100);
125 _progressBar.setValue(0);
126 _progressBar.setStringPainted(true);
127 _progressBar.setString("");
128 JPanel panel = new JPanel();
129 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
130 panel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
131 panel.add(new JLabel(I18nManager.getText("dialog.jpegload.progress")));
132 panel.add(_progressBar);
133 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
134 cancelButton.addActionListener(new ActionListener() {
135 public void actionPerformed(ActionEvent e)
140 panel.add(cancelButton);
141 _progressDialog.getContentPane().add(panel);
142 _progressDialog.pack();
147 * Run method for performing tasks in separate thread
151 // Initialise arrays, errors, summaries
152 _fileCounts = new int[3]; // files, jpegs, gps
153 _photos = new TreeSet<Photo>(new MediaSorter());
154 File[] files = _fileChooser.getSelectedFiles();
155 // Loop recursively over selected files/directories to count files
156 int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
157 // Set up the progress bar for this number of files
158 _progressBar.setMaximum(numFiles);
159 _progressBar.setValue(0);
162 // Process the files recursively and build lists of photos
163 processFileList(files, true, _subdirCheckbox.isSelected());
164 _progressDialog.setVisible(false);
165 _progressDialog.dispose(); // Sometimes dialog doesn't disappear without this dispose
166 if (_cancelled) {return;}
168 if (_fileCounts[0] == 0)
170 // No files found at all
171 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
173 else if (_fileCounts[1] == 0)
176 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
178 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
180 // Need coordinates but no gps information found
181 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
185 // Found some photos to load - pass information back to app
186 _app.informPhotosLoaded(_photos);
192 * Process a list of files and/or directories
193 * @param inFiles array of file/directories
194 * @param inFirstDir true if first directory
195 * @param inDescend true to descend to subdirectories
197 private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
201 // Loop over elements in array
202 for (int i=0; i<inFiles.length && !_cancelled; i++)
204 File file = inFiles[i];
205 if (file.exists() && file.canRead())
207 // Check whether it's a file or a directory
212 else if (file.isDirectory() && (inFirstDir || inDescend))
214 // Always process first directory,
215 // only process subdirectories if checkbox selected
216 File[] files = file.listFiles();
217 processFileList(files, false, inDescend);
220 // if file doesn't exist or isn't readable - ignore
227 * Process the given file, by attempting to extract its tags
228 * @param inFile file object to read
230 private void processFile(File inFile)
232 // Update progress bar
233 _fileCounts[0]++; // file found
234 _progressBar.setValue(_fileCounts[0]);
235 _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum());
236 _progressBar.repaint();
238 // Check whether filename corresponds with accepted filenames
239 if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
240 // If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
242 if (inFile.exists() && inFile.canRead()) {
243 _fileCounts[1]++; // jpeg found
245 Photo photo = createPhoto(inFile);
246 if (photo.getDataPoint() != null) {
247 _fileCounts[2]++; // photo has coordinates
249 // Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
250 if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
251 && (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
252 || _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
259 * Create a Photo object for the given file, including reading exif information
260 * @param inFile file object
261 * @return Photo object
263 public static Photo createPhoto(File inFile)
265 // Create Photo object
266 Photo photo = new Photo(inFile);
267 // Try to get information out of exif
268 JpegData jpegData = ExifGateway.getJpegData(inFile);
269 Timestamp timestamp = null;
270 if (jpegData != null)
272 if (jpegData.isGpsValid())
274 timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
275 // Make DataPoint and attach to Photo
276 DataPoint point = createDataPoint(jpegData);
277 point.setPhoto(photo);
278 point.setSegmentStart(true);
279 photo.setDataPoint(point);
280 photo.setOriginalStatus(Photo.Status.TAGGED);
282 // Use exif timestamp if gps timestamp not available
283 if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
284 timestamp = createTimestamp(jpegData.getOriginalTimestamp());
286 if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
287 timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
289 photo.setExifThumbnail(jpegData.getThumbnailImage());
290 // Also extract orientation tag for setting rotation state of photo
291 photo.setRotation(jpegData.getRequiredRotation());
293 // Use file timestamp if exif timestamp isn't available
294 if (timestamp == null) {
295 timestamp = new Timestamp(inFile.lastModified());
297 // Apply timestamp to photo and its point (if any)
298 photo.setTimestamp(timestamp);
299 if (photo.getDataPoint() != null) {
300 photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.FORMAT_ISO_8601), false);
307 * Recursively count the selected Files so we can draw a progress bar
308 * @param inFiles file list
309 * @param inFirstDir true if first directory
310 * @param inDescend true to descend to subdirectories
311 * @return count of the files selected
313 private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
318 // Loop over elements in array
319 for (int i=0; i<inFiles.length; i++)
321 File file = inFiles[i];
322 if (file.exists() && file.canRead())
324 // Store first directory in config for later
325 if (i == 0 && inFirstDir) {
326 File workingDir = file.isDirectory()?file:file.getParentFile();
327 Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
329 // Check whether it's a file or a directory
334 else if (file.isDirectory() && (inFirstDir || inDescend))
336 fileCount += countFileList(file.listFiles(), false, inDescend);
346 * Create a DataPoint object from the given jpeg data
347 * @param inData Jpeg data including coordinates
348 * @return DataPoint object for Track
350 private static DataPoint createDataPoint(JpegData inData)
352 // Create model objects from jpeg data
353 double latval = getCoordinateDoubleValue(inData.getLatitude(),
354 inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
355 Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
356 double lonval = getCoordinateDoubleValue(inData.getLongitude(),
357 inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
358 Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
359 Altitude altitude = null;
360 if (inData.hasAltitude()) {
361 altitude = new Altitude(inData.getAltitude(), Altitude.Format.METRES);
363 return new DataPoint(latitude, longitude, altitude);
368 * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
369 * @param inValues array of three doubles for deg-min-sec
370 * @param isPositive true for positive hemisphere, for positive double value
371 * @return double value of coordinate, either positive or negative
373 private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
375 if (inValues == null || inValues.length != 3) return 0.0;
376 double value = inValues[0] // degrees
377 + inValues[1] / 60.0 // minutes
378 + inValues[2] / 60.0 / 60.0; // seconds
379 // make sure it's the correct sign
380 value = Math.abs(value);
381 if (!isPositive) value = -value;
387 * Use the given int values to create a timestamp
388 * @param inDate ints describing date
389 * @param inTime ints describing time
390 * @return Timestamp object corresponding to inputs
392 private static Timestamp createTimestamp(int[] inDate, int[] inTime)
394 if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
397 return new Timestamp(inDate[0], inDate[1], inDate[2],
398 inTime[0], inTime[1], inTime[2]);
403 * Use the given String value to create a timestamp
404 * @param inStamp timestamp from exif
405 * @return Timestamp object corresponding to input
407 private static Timestamp createTimestamp(String inStamp)
409 Timestamp stamp = null;
412 stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
413 Integer.parseInt(inStamp.substring(5, 7)),
414 Integer.parseInt(inStamp.substring(8, 10)),
415 Integer.parseInt(inStamp.substring(11, 13)),
416 Integer.parseInt(inStamp.substring(14, 16)),
417 Integer.parseInt(inStamp.substring(17)));
419 catch (NumberFormatException nfe) {}