]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/load/JpegLoader.java
Version 11, August 2010
[GpsPrune.git] / tim / prune / load / JpegLoader.java
index 6fe52a18a96e48be328a83f5bb0320cd4c463fce..feec754f22f7049dc7bcef4b26d71cba534d2ed2 100644 (file)
@@ -3,7 +3,7 @@ package tim.prune.load;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.File;
-import java.util.ArrayList;
+import java.util.TreeSet;
 
 import javax.swing.BorderFactory;
 import javax.swing.BoxLayout;
@@ -13,21 +13,22 @@ import javax.swing.JDialog;
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JProgressBar;
 
 import tim.prune.App;
 import tim.prune.I18nManager;
+import tim.prune.config.Config;
 import tim.prune.data.Altitude;
 import tim.prune.data.DataPoint;
+import tim.prune.data.Field;
+import tim.prune.data.LatLonRectangle;
 import tim.prune.data.Latitude;
 import tim.prune.data.Longitude;
 import tim.prune.data.Photo;
-import tim.prune.drew.jpeg.ExifReader;
-import tim.prune.drew.jpeg.JpegData;
-import tim.prune.drew.jpeg.JpegException;
-import tim.prune.drew.jpeg.Rational;
+import tim.prune.data.Timestamp;
+import tim.prune.jpeg.ExifGateway;
+import tim.prune.jpeg.JpegData;
 
 /**
  * Class to manage the loading of Jpegs and dealing with the GPS data from them
@@ -37,12 +38,16 @@ public class JpegLoader implements Runnable
        private App _app = null;
        private JFrame _parentFrame = null;
        private JFileChooser _fileChooser = null;
+       private GenericFileFilter _fileFilter = null;
        private JCheckBox _subdirCheckbox = null;
+       private JCheckBox _noExifCheckbox = null;
+       private JCheckBox _outsideAreaCheckbox = null;
        private JDialog _progressDialog   = null;
        private JProgressBar _progressBar = null;
        private int[] _fileCounts = null;
        private boolean _cancelled = false;
-       private ArrayList _photos = null;
+       private LatLonRectangle _trackRectangle = null;
+       private TreeSet<Photo> _photos = null;
 
 
        /**
@@ -54,36 +59,66 @@ public class JpegLoader implements Runnable
        {
                _app = inApp;
                _parentFrame = inParentFrame;
+               String[] fileTypes = {"jpg", "jpe", "jpeg"};
+               _fileFilter = new GenericFileFilter("filetype.jpeg", fileTypes);
        }
 
+
        /**
-        * Select an input file and open the GUI frame
-        * to select load options
+        * Open the GUI to select options and start the load
+        * @param inRectangle track rectangle
         */
-       public void openFile()
+       public void openDialog(LatLonRectangle inRectangle)
        {
+               // Create file chooser if necessary
                if (_fileChooser == null)
                {
                        _fileChooser = new JFileChooser();
                        _fileChooser.setMultiSelectionEnabled(true);
                        _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+                       _fileChooser.setFileFilter(_fileFilter);
+                       _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
                        _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
                        _subdirCheckbox.setSelected(true);
-                       _fileChooser.setAccessory(_subdirCheckbox);
+                       _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
+                       _noExifCheckbox.setSelected(true);
+                       _outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
+                       _outsideAreaCheckbox.setSelected(true);
+                       JPanel panel = new JPanel();
+                       panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+                       panel.add(_subdirCheckbox);
+                       panel.add(_noExifCheckbox);
+                       panel.add(_outsideAreaCheckbox);
+                       _fileChooser.setAccessory(panel);
+                       // start from directory in config if already set by other operations
+                       String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
+                       if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
+                       if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
                }
+               // enable/disable track checkbox
+               _trackRectangle = inRectangle;
+               _outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
+               // Show file dialog to choose file / directory(ies)
                if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
                {
                        // Bring up dialog before starting
-                       showDialog();
+                       if (_progressDialog == null) {
+                               createProgressDialog();
+                       }
+                       // reset dialog and show it
+                       _progressBar.setValue(0);
+                       _progressBar.setString("");
+                       _progressDialog.setVisible(true);
+                       // start thread for processing
                        new Thread(this).start();
                }
        }
 
 
        /**
-        * Show the main dialog
+        * Create the dialog to show the progress
         */
-       private void showDialog()
+       private void createProgressDialog()
        {
                _progressDialog = new JDialog(_parentFrame, I18nManager.getText("dialog.jpegload.progress.title"));
                _progressDialog.setLocationRelativeTo(_parentFrame);
@@ -106,7 +141,6 @@ public class JpegLoader implements Runnable
                panel.add(cancelButton);
                _progressDialog.getContentPane().add(panel);
                _progressDialog.pack();
-               _progressDialog.show();
        }
 
 
@@ -117,41 +151,46 @@ public class JpegLoader implements Runnable
        {
                // Initialise arrays, errors, summaries
                _fileCounts = new int[4]; // files, jpegs, exifs, gps
-               _photos = new ArrayList();
-               // Loop over selected files/directories
+               _photos = new TreeSet<Photo>(new PhotoSorter());
                File[] files = _fileChooser.getSelectedFiles();
+               // Loop recursively over selected files/directories to count files
                int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
-               // if (false) System.out.println("Found " + numFiles + " files");
+               // Set up the progress bar for this number of files
                _progressBar.setMaximum(numFiles);
                _progressBar.setValue(0);
                _cancelled = false;
+
+               // Process the files recursively and build lists of photos
                processFileList(files, true, _subdirCheckbox.isSelected());
-               _progressDialog.hide();
-               if (_cancelled) return;
-               // System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1] + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
+               _progressDialog.setVisible(false);
+               _progressDialog.dispose(); // Sometimes dialog doesn't disappear without this dispose
+               if (_cancelled) {return;}
+
+               //System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1]
+               //  + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
                if (_fileCounts[0] == 0)
                {
-                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"),
-                               I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+                       // No files found at all
+                       _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
                }
                else if (_fileCounts[1] == 0)
                {
-                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"),
-                               I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+                       // No jpegs found
+                       _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
                }
-               else if (_fileCounts[2] == 0)
+               else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
                {
-                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.noexiffound"),
-                               I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+                       // Need coordinates but no exif found
+                       _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.noexiffound");
                }
-               else if (_fileCounts[3] == 0)
+               else if (!_noExifCheckbox.isSelected() && _fileCounts[3] == 0)
                {
-                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nogpsfound"),
-                               I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+                       // Need coordinates but no gps information found
+                       _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
                }
                else
                {
-                       // Load information into dialog for confirmation
+                       // Found some photos to load - pass information back to app
                        _app.informPhotosLoaded(_photos);
                }
        }
@@ -168,7 +207,7 @@ public class JpegLoader implements Runnable
                if (inFiles != null)
                {
                        // Loop over elements in array
-                       for (int i=0; i<inFiles.length; i++)
+                       for (int i=0; i<inFiles.length && !_cancelled; i++)
                        {
                                File file = inFiles[i];
                                if (file.exists() && file.canRead())
@@ -182,15 +221,11 @@ public class JpegLoader implements Runnable
                                        {
                                                // Always process first directory,
                                                // only process subdirectories if checkbox selected
-                                               processDirectory(file, inDescend);
+                                               File[] files = file.listFiles();
+                                               processFileList(files, false, inDescend);
                                        }
                                }
-                               else
-                               {
-                                       // file doesn't exist or isn't readable - record error
-                               }
-                               // check for cancel
-                               if (_cancelled) break;
+                               // if file doesn't exist or isn't readable - ignore
                        }
                }
        }
@@ -202,52 +237,66 @@ public class JpegLoader implements Runnable
         */
        private void processFile(File inFile)
        {
+               // Update progress bar
                _fileCounts[0]++; // file found
                _progressBar.setValue(_fileCounts[0]);
                _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum());
                _progressBar.repaint();
-               try
+
+               // Check whether filename corresponds with accepted filenames
+               if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
+               // If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
+
+               // Create Photo object
+               Photo photo = new Photo(inFile);
+               if (inFile.exists() && inFile.canRead()) {
+                       _fileCounts[1]++; // jpeg found
+               }
+               // Try to get information out of exif
+               JpegData jpegData = ExifGateway.getJpegData(inFile);
+               Timestamp timestamp = null;
+               if (jpegData != null)
                {
-                       JpegData jpegData = new ExifReader(inFile).extract();
-                       _fileCounts[1]++; // jpeg found (no exception thrown)
-//                     if (jpegData.getNumErrors() > 0)
-//                             System.out.println("Number of errors was: " + jpegData.getNumErrors() + ": " + jpegData.getErrors().get(0));
                        if (jpegData.getExifDataPresent())
-                               _fileCounts[2]++; // exif found
-                       if (jpegData.isValid())
+                               {_fileCounts[2]++;} // exif found
+                       if (jpegData.isGpsValid())
                        {
-//                             if (false && jpegData.getTimestamp() != null)
-//                                     System.out.println("Timestamp is " + jpegData.getTimestamp()[0].toString() + ":" + jpegData.getTimestamp()[1].toString() + ":" + jpegData.getTimestamp()[2].toString());
-//                             if (false && jpegData.getDatestamp() != null)
-//                                     System.out.println("Datestamp is " + jpegData.getDatestamp()[0].toString() + ":" + jpegData.getDatestamp()[1].toString() + ":" + jpegData.getDatestamp()[2].toString());
-                               // Make DataPoint and Photo
+                               timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
+                               // Make DataPoint and attach to Photo
                                DataPoint point = createDataPoint(jpegData);
-                               Photo photo = new Photo(inFile);
                                point.setPhoto(photo);
+                               point.setSegmentStart(true);
                                photo.setDataPoint(point);
-                               _photos.add(photo);
-//                             System.out.println("Made photo: " + photo.getFile().getAbsolutePath() + " with the datapoint: "
-//                                     + point.getLatitude().output(Latitude.FORMAT_DEG_MIN_SEC) + ", "
-//                                     + point.getLongitude().output(Longitude.FORMAT_DEG_MIN_SEC) + ", "
-//                                     + point.getAltitude().getValue(Altitude.FORMAT_METRES));
+                               photo.setOriginalStatus(Photo.Status.TAGGED);
                                _fileCounts[3]++;
                        }
+                       // Use exif timestamp if gps timestamp not available
+                       if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
+                               timestamp = createTimestamp(jpegData.getOriginalTimestamp());
+                       }
+                       if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
+                               timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
+                       }
+                       photo.setExifThumbnail(jpegData.getThumbnailImage());
+                       // Also extract orientation tag for setting rotation state of photo
+                       photo.setRotation(jpegData.getRequiredRotation());
                }
-               catch (JpegException jpe) { // don't list errors, just count them
+               // Use file timestamp if exif timestamp isn't available
+               if (timestamp == null) {
+                       timestamp = new Timestamp(inFile.lastModified());
+               }
+               // Apply timestamp to photo and its point (if any)
+               photo.setTimestamp(timestamp);
+               if (photo.getDataPoint() != null) {
+                       photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.FORMAT_ISO_8601), false);
+               }
+               // Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
+               if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
+                       && (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
+                               || _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
+               {
+                       _photos.add(photo);
                }
-       }
-
-
-       /**
-        * Process the given directory, by looping over its contents
-        * and recursively through its subdirectories
-        * @param inDirectory directory to read
-        * @param inDescend true to descend subdirectories
-        */
-       private void processDirectory(File inDirectory, boolean inDescend)
-       {
-               File[] files = inDirectory.listFiles();
-               processFileList(files, false, inDescend);
        }
 
 
@@ -269,6 +318,11 @@ public class JpegLoader implements Runnable
                                File file = inFiles[i];
                                if (file.exists() && file.canRead())
                                {
+                                       // Store first directory in config for later
+                                       if (i == 0 && inFirstDir) {
+                                               File workingDir = file.isDirectory()?file:file.getParentFile();
+                                               Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
+                                       }
                                        // Check whether it's a file or a directory
                                        if (file.isFile())
                                        {
@@ -295,30 +349,71 @@ public class JpegLoader implements Runnable
                // Create model objects from jpeg data
                double latval = getCoordinateDoubleValue(inData.getLatitude(),
                        inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
-               Latitude latitude = new Latitude(latval, Latitude.FORMAT_NONE);
+               Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
                double lonval = getCoordinateDoubleValue(inData.getLongitude(),
                        inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
-               Longitude longitude = new Longitude(lonval, Longitude.FORMAT_NONE);
-               Altitude altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES);
+               Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
+               Altitude altitude = null;
+               if (inData.hasAltitude()) {
+                       altitude = new Altitude(inData.getAltitude(), Altitude.Format.METRES);
+               }
                return new DataPoint(latitude, longitude, altitude);
        }
 
 
        /**
-        * Convert an array of 3 Rational numbers into a double coordinate value
-        * @param inRationals array of three Rational objects
+        * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
+        * @param inValues array of three doubles for deg-min-sec
         * @param isPositive true for positive hemisphere, for positive double value
         * @return double value of coordinate, either positive or negative
         */
-       private static double getCoordinateDoubleValue(Rational[] inRationals, boolean isPositive)
+       private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
        {
-               if (inRationals == null || inRationals.length != 3) return 0.0;
-               double value = inRationals[0].doubleValue()        // degrees
-                       + inRationals[1].doubleValue() / 60.0          // minutes
-                       + inRationals[2].doubleValue() / 60.0 / 60.0;  // seconds
+               if (inValues == null || inValues.length != 3) return 0.0;
+               double value = inValues[0]        // degrees
+                       + inValues[1] / 60.0          // minutes
+                       + inValues[2] / 60.0 / 60.0;  // seconds
                // make sure it's the correct sign
                value = Math.abs(value);
                if (!isPositive) value = -value;
                return value;
        }
+
+
+       /**
+        * Use the given int values to create a timestamp
+        * @param inDate ints describing date
+        * @param inTime ints describing time
+        * @return Timestamp object corresponding to inputs
+        */
+       private static Timestamp createTimestamp(int[] inDate, int[] inTime)
+       {
+               if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
+                       return null;
+               }
+               return new Timestamp(inDate[0], inDate[1], inDate[2],
+                       inTime[0], inTime[1], inTime[2]);
+       }
+
+
+       /**
+        * Use the given String value to create a timestamp
+        * @param inStamp timestamp from exif
+        * @return Timestamp object corresponding to input
+        */
+       private static Timestamp createTimestamp(String inStamp)
+       {
+               Timestamp stamp = null;
+               try
+               {
+                       stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
+                               Integer.parseInt(inStamp.substring(5, 7)),
+                               Integer.parseInt(inStamp.substring(8, 10)),
+                               Integer.parseInt(inStamp.substring(11, 13)),
+                               Integer.parseInt(inStamp.substring(14, 16)),
+                               Integer.parseInt(inStamp.substring(17)));
+               }
+               catch (NumberFormatException nfe) {}
+               return stamp;
+       }
 }