1 package tim.prune.load;
4 import java.util.TreeSet;
6 import javax.swing.BoxLayout;
7 import javax.swing.JCheckBox;
8 import javax.swing.JFileChooser;
9 import javax.swing.JFrame;
10 import javax.swing.JPanel;
13 import tim.prune.I18nManager;
14 import tim.prune.config.Config;
15 import tim.prune.data.Altitude;
16 import tim.prune.data.DataPoint;
17 import tim.prune.data.LatLonRectangle;
18 import tim.prune.data.Latitude;
19 import tim.prune.data.Longitude;
20 import tim.prune.data.Photo;
21 import tim.prune.data.Timestamp;
22 import tim.prune.data.TimestampLocal;
23 import tim.prune.data.TimestampUtc;
24 import tim.prune.data.UnitSetLibrary;
25 import tim.prune.function.Cancellable;
26 import tim.prune.jpeg.InternalExifLibrary;
27 import tim.prune.jpeg.JpegData;
30 * Class to manage the loading of Jpegs and dealing with the GPS data from them
32 public class JpegLoader implements Runnable, Cancellable
34 private App _app = null;
35 private JFrame _parentFrame = null;
36 private JFileChooser _fileChooser = null;
37 private GenericFileFilter _fileFilter = null;
38 private JCheckBox _subdirCheckbox = null;
39 private JCheckBox _noExifCheckbox = null;
40 private JCheckBox _outsideAreaCheckbox = null;
41 private MediaLoadProgressDialog _progressDialog = null;
42 private int[] _fileCounts = null;
43 private boolean _cancelled = false;
44 private LatLonRectangle _trackRectangle = null;
45 private TreeSet<Photo> _photos = null;
50 * @param inApp Application object to inform of photo load
51 * @param inParentFrame parent frame to reference for dialogs
53 public JpegLoader(App inApp, JFrame inParentFrame)
56 _parentFrame = inParentFrame;
57 _fileFilter = new JpegFileFilter();
62 * Open the GUI to select options and start the load
63 * @param inRectangle track rectangle
65 public void openDialog(LatLonRectangle inRectangle)
67 // Create file chooser if necessary
68 if (_fileChooser == null)
70 _fileChooser = new JFileChooser();
71 _fileChooser.setMultiSelectionEnabled(true);
72 _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
73 _fileChooser.setFileFilter(_fileFilter);
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 _outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
80 _outsideAreaCheckbox.setSelected(true);
81 JPanel panel = new JPanel();
82 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
83 panel.add(_subdirCheckbox);
84 panel.add(_noExifCheckbox);
85 panel.add(_outsideAreaCheckbox);
86 _fileChooser.setAccessory(panel);
87 // start from directory in config if already set by other operations
88 String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
89 if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
90 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
92 // enable/disable track checkbox
93 _trackRectangle = inRectangle;
94 _outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
95 // Show file dialog to choose file / directory(ies)
96 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
98 // Bring up dialog before starting
99 _progressDialog = new MediaLoadProgressDialog(_parentFrame, this);
100 _progressDialog.show();
101 // start thread for processing
102 new Thread(this).start();
107 public void cancel() {
113 * Run method for performing tasks in separate thread
117 // Initialise arrays, errors, summaries
118 _fileCounts = new int[3]; // files, jpegs, gps
119 _photos = new TreeSet<Photo>(new MediaSorter());
120 File[] files = _fileChooser.getSelectedFiles();
121 // Loop recursively over selected files/directories to count files
122 int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
123 // Set up the progress bar for this number of files
124 _progressDialog.showProgress(0, numFiles);
127 // Process the files recursively and build lists of photos
128 processFileList(files, true, _subdirCheckbox.isSelected());
129 _progressDialog.close();
130 if (_cancelled) {return;}
132 if (_fileCounts[0] == 0)
134 // No files found at all
135 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
137 else if (_fileCounts[1] == 0)
140 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
142 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
144 // Need coordinates but no gps information found
145 _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
149 // Found some photos to load - pass information back to app
150 _app.informPhotosLoaded(_photos);
156 * Process a list of files and/or directories
157 * @param inFiles array of file/directories
158 * @param inFirstDir true if first directory
159 * @param inDescend true to descend to subdirectories
161 private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
163 if (inFiles == null) return;
164 // Loop over elements in array
165 for (int i=0; i<inFiles.length && !_cancelled; i++)
167 File file = inFiles[i];
168 if (file.exists() && file.canRead())
170 // Check whether it's a file or a directory
175 else if (file.isDirectory() && (inFirstDir || inDescend))
177 // Always process first directory,
178 // only process subdirectories if checkbox selected
179 File[] files = file.listFiles();
180 processFileList(files, false, inDescend);
183 // if file doesn't exist or isn't readable - ignore
189 * Process the given file, by attempting to extract its tags
190 * @param inFile file object to read
192 private void processFile(File inFile)
194 // Update progress bar
195 _fileCounts[0]++; // file found
196 _progressDialog.showProgress(_fileCounts[0], -1);
198 // Check whether filename corresponds with accepted filenames
199 if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
200 // If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
202 if (inFile.exists() && inFile.canRead()) {
203 _fileCounts[1]++; // jpeg found
205 Photo photo = createPhoto(inFile);
206 if (photo.getDataPoint() != null) {
207 _fileCounts[2]++; // photo has coordinates
209 // Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
210 if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
211 && (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
212 || _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
219 * Create a Photo object for the given file, including reading exif information
220 * @param inFile file object
221 * @return Photo object
223 public static Photo createPhoto(File inFile)
225 // Create Photo object
226 Photo photo = new Photo(inFile);
227 // Try to get information out of exif
228 JpegData jpegData = new InternalExifLibrary().getJpegData(inFile);
229 Timestamp timestamp = null;
230 if (jpegData != null)
232 if (jpegData.isGpsValid())
234 timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
235 // Make DataPoint and attach to Photo
236 DataPoint point = createDataPoint(jpegData);
237 point.setPhoto(photo);
238 point.setSegmentStart(true);
239 photo.setDataPoint(point);
240 photo.setOriginalStatus(Photo.Status.TAGGED);
242 // Use exif timestamp if gps timestamp not available
243 if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
244 timestamp = createTimestamp(jpegData.getOriginalTimestamp());
246 if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
247 timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
249 photo.setExifThumbnail(jpegData.getThumbnailImage());
250 // Also extract orientation tag for setting rotation state of photo
251 photo.setRotation(jpegData.getRequiredRotation());
252 // Set bearing, if any
253 photo.setBearing(jpegData.getBearing());
255 // Use file timestamp if exif timestamp isn't available
256 if (timestamp == null) {
257 timestamp = new TimestampUtc(inFile.lastModified());
259 // Apply timestamp to photo and its point (if any)
260 photo.setTimestamp(timestamp);
261 if (photo.getDataPoint() != null) {
262 // photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.Format.ISO8601), false);
269 * Recursively count the selected Files so we can draw a progress bar
270 * @param inFiles file list
271 * @param inFirstDir true if first directory
272 * @param inDescend true to descend to subdirectories
273 * @return count of the files selected
275 private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
280 // Loop over elements in array
281 for (int i=0; i<inFiles.length; i++)
283 File file = inFiles[i];
284 if (file.exists() && file.canRead())
286 // Store first directory in config for later
287 if (i == 0 && inFirstDir) {
288 File workingDir = file.isDirectory()?file:file.getParentFile();
289 Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
291 // Check whether it's a file or a directory
296 else if (file.isDirectory() && (inFirstDir || inDescend))
298 fileCount += countFileList(file.listFiles(), false, inDescend);
308 * Create a DataPoint object from the given jpeg data
309 * @param inData Jpeg data including coordinates
310 * @return DataPoint object for Track
312 private static DataPoint createDataPoint(JpegData inData)
314 // Create model objects from jpeg data
315 double latval = getCoordinateDoubleValue(inData.getLatitude(),
316 inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
317 Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
318 double lonval = getCoordinateDoubleValue(inData.getLongitude(),
319 inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
320 Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
321 Altitude altitude = null;
322 if (inData.hasAltitude()) {
323 altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
325 return new DataPoint(latitude, longitude, altitude);
330 * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
331 * @param inValues array of three doubles for deg-min-sec
332 * @param isPositive true for positive hemisphere, for positive double value
333 * @return double value of coordinate, either positive or negative
335 private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
337 if (inValues == null || inValues.length != 3) return 0.0;
338 double value = inValues[0] // degrees
339 + inValues[1] / 60.0 // minutes
340 + inValues[2] / 60.0 / 60.0; // seconds
341 // make sure it's the correct sign
342 value = Math.abs(value);
343 if (!isPositive) value = -value;
349 * Use the given int values to create a timestamp
350 * @param inDate ints describing date
351 * @param inTime ints describing time
352 * @return Timestamp object corresponding to inputs
354 private static Timestamp createTimestamp(int[] inDate, int[] inTime)
356 if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
359 return new TimestampLocal(inDate[0], inDate[1], inDate[2],
360 inTime[0], inTime[1], inTime[2]);
365 * Use the given String value to create a timestamp
366 * @param inStamp timestamp from exif
367 * @return Timestamp object corresponding to input
369 private static Timestamp createTimestamp(String inStamp)
371 Timestamp stamp = null;
374 stamp = new TimestampLocal(Integer.parseInt(inStamp.substring(0, 4)),
375 Integer.parseInt(inStamp.substring(5, 7)),
376 Integer.parseInt(inStamp.substring(8, 10)),
377 Integer.parseInt(inStamp.substring(11, 13)),
378 Integer.parseInt(inStamp.substring(14, 16)),
379 Integer.parseInt(inStamp.substring(17)));
381 catch (NumberFormatException nfe) {}