]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/save/ExifSaver.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / save / ExifSaver.java
diff --git a/src/tim/prune/save/ExifSaver.java b/src/tim/prune/save/ExifSaver.java
new file mode 100644 (file)
index 0000000..d17c5d4
--- /dev/null
@@ -0,0 +1,420 @@
+package tim.prune.save;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+
+import tim.prune.ExternalTools;
+import tim.prune.I18nManager;
+import tim.prune.UpdateMessageBroker;
+import tim.prune.config.Config;
+import tim.prune.data.Coordinate;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoList;
+
+/**
+ * Class to call Exiftool to save coordinate information in jpg files
+ */
+public class ExifSaver implements Runnable
+{
+       private Frame _parentFrame = null;
+       private JDialog _dialog = null;
+       private JButton _okButton = null;
+       private JCheckBox _overwriteCheckbox = null;
+       private JCheckBox _forceCheckbox = null;
+       private JProgressBar _progressBar = null;
+       private PhotoTableModel _photoTableModel = null;
+       private boolean _saveCancelled = false;
+
+
+       // To preserve timestamps of file use parameter -P
+       // To overwrite file (careful!) use parameter -overwrite_original_in_place
+
+       // To read all GPS tags,   use -GPS:All
+       // To delete all GPS tags, use -GPS:All=
+
+       // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
+       // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
+
+       // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
+
+       // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
+       // (setting altitude ref to 0 doesn't work)
+       // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
+       // (latitude as space-separated deg min sec, reference as either N or S)
+       // Same for longitude, reference E or W
+
+
+       /**
+        * Constructor
+        * @param inParentFrame parent frame
+        */
+       public ExifSaver(Frame inParentFrame)
+       {
+               _parentFrame = inParentFrame;
+       }
+
+
+       /**
+        * Save exif information to all photos in the list
+        * whose coordinate information has changed since loading
+        * @param inPhotoList list of photos to save
+        * @return true if saved
+        */
+       public boolean saveExifInformation(PhotoList inPhotoList)
+       {
+               // Check if external exif tool can be called
+               boolean exifToolInstalled = ExternalTools.isToolInstalled(ExternalTools.TOOL_EXIFTOOL);
+               if (!exifToolInstalled)
+               {
+                       // show warning
+                       int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
+                               I18nManager.getText("dialog.saveexif.title"),
+                               JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+                       if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
+                       {
+                               return false;
+                       }
+               }
+               // Make model and add all photos to it
+               _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
+               for (int i=0; i<inPhotoList.getNumPhotos(); i++)
+               {
+                       Photo photo = inPhotoList.getPhoto(i);
+                       PhotoTableEntry entry = new PhotoTableEntry(photo);
+                       _photoTableModel.addPhotoInfo(entry);
+               }
+               // Check if there are any modified photos to save
+               if (_photoTableModel.getNumSaveablePhotos() < 1)
+               {
+                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
+                       return false;
+               }
+               // Construct dialog
+               _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
+               _dialog.setLocationRelativeTo(_parentFrame);
+               _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+               _dialog.getContentPane().add(makeDialogComponents());
+               _dialog.pack();
+               // set progress bar and show dialog
+               _progressBar.setVisible(false);
+               _dialog.setVisible(true);
+               return true;
+       }
+
+
+       /**
+        * Put together the dialog components for adding to the gui
+        * @return panel containing all gui components
+        */
+       private JPanel makeDialogComponents()
+       {
+               JPanel panel = new JPanel();
+               panel.setLayout(new BorderLayout());
+               // Label at top
+               JLabel topLabel = new JLabel(I18nManager.getText("dialog.saveexif.intro"));
+               topLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
+               panel.add(topLabel, BorderLayout.NORTH);
+               // centre panel with most controls
+               JPanel centrePanel = new JPanel();
+               centrePanel.setLayout(new BorderLayout());
+               // table panel with table and checkbox
+               JPanel tablePanel = new JPanel();
+               tablePanel.setLayout(new BorderLayout());
+               JTable photoTable = new JTable(_photoTableModel);
+               JScrollPane scrollPane = new JScrollPane(photoTable);
+               scrollPane.setPreferredSize(new Dimension(300, 160));
+               tablePanel.add(scrollPane, BorderLayout.CENTER);
+               // Pair of checkboxes
+               JPanel checkPanel = new JPanel();
+               checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
+               _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
+               _overwriteCheckbox.setSelected(false);
+               checkPanel.add(_overwriteCheckbox);
+               _forceCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.force"));
+               _forceCheckbox.setSelected(false);
+               checkPanel.add(_forceCheckbox);
+               tablePanel.add(checkPanel, BorderLayout.SOUTH);
+               centrePanel.add(tablePanel, BorderLayout.CENTER);
+               // progress bar below main controls
+               _progressBar = new JProgressBar(0, 100);
+               centrePanel.add(_progressBar, BorderLayout.SOUTH);
+               panel.add(centrePanel, BorderLayout.CENTER);
+               // Right-hand panel with select all, none buttons
+               JPanel rightPanel = new JPanel();
+               rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
+               JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
+               selectAllButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               selectPhotos(true);
+                       }
+               });
+               rightPanel.add(selectAllButton);
+               JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
+               selectNoneButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               selectPhotos(false);
+                       }
+               });
+               rightPanel.add(selectNoneButton);
+               panel.add(rightPanel, BorderLayout.EAST);
+               // Lower panel with ok and cancel buttons
+               JPanel buttonPanel = new JPanel();
+               buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+               _okButton = new JButton(I18nManager.getText("button.ok"));
+               _okButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // disable ok button
+                               _okButton.setEnabled(false);
+                               // start new thread to do save
+                               new Thread(ExifSaver.this).start();
+                       }
+               });
+               buttonPanel.add(_okButton);
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _saveCancelled = true;
+                               _dialog.dispose();
+                       }
+               });
+               buttonPanel.add(cancelButton);
+               panel.add(buttonPanel, BorderLayout.SOUTH);
+               return panel;
+       }
+
+
+       /**
+        * Select all or select none
+        * @param inSelected true to select all photos, false to deselect all
+        */
+       private void selectPhotos(boolean inSelected)
+       {
+               int numPhotos = _photoTableModel.getRowCount();
+               for (int i=0; i<numPhotos; i++)
+               {
+                       _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
+               }
+               _photoTableModel.fireTableDataChanged();
+       }
+
+
+       /**
+        * Run method for saving in separate thread
+        */
+       public void run()
+       {
+               _saveCancelled = false;
+               PhotoTableEntry entry = null;
+               Photo photo = null;
+               int numPhotos = _photoTableModel.getRowCount();
+               _progressBar.setMaximum(numPhotos);
+               _progressBar.setValue(0);
+               _progressBar.setVisible(true);
+               boolean overwriteFlag = _overwriteCheckbox.isSelected();
+               int numSaved = 0, numFailed = 0, numForced = 0;
+               // Loop over all photos in list
+               for (int i=0; i<numPhotos; i++)
+               {
+                       entry = _photoTableModel.getPhotoTableEntry(i);
+                       if (entry != null && entry.getSaveFlag() && !_saveCancelled)
+                       {
+                               // Only look at photos which are selected and whose status has changed since load
+                               photo = entry.getPhoto();
+                               if (photo != null && photo.isModified())
+                               {
+                                       // Increment counter if save successful
+                                       if (savePhoto(photo, overwriteFlag, false)) {
+                                               numSaved++;
+                                       }
+                                       else {
+                                               if (_forceCheckbox.isSelected() && savePhoto(photo, overwriteFlag, true))
+                                               {
+                                                       numForced++;
+                                               }
+                                               else {
+                                                       numFailed++;
+                                               }
+                                       }
+                               }
+                       }
+                       // update progress bar
+                       _progressBar.setValue(i + 1);
+               }
+               _progressBar.setVisible(false);
+               // Show confirmation
+               UpdateMessageBroker.informSubscribers(I18nManager.getTextWithNumber("confirm.saveexif.ok", numSaved));
+               if (numFailed > 0)
+               {
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getTextWithNumber("error.saveexif.failed", numFailed),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
+               }
+               if (numForced > 0)
+               {
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getTextWithNumber("error.saveexif.forced", numForced),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
+               }
+               // close dialog, all finished
+               _dialog.dispose();
+       }
+
+
+       /**
+        * Save the details for the given photo
+        * @param inPhoto Photo object
+        * @param inOverwriteFlag true to overwrite file, false otherwise
+        * @param inForceFlag true to force write, ignoring minor errors
+        * @return true if details saved ok
+        */
+       private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag, boolean inForceFlag)
+       {
+               // If photos don't have a file, then can't save them
+               if (inPhoto.getFile() == null) {
+                       return false;
+               }
+               // Check whether photo file still exists
+               if (!inPhoto.getFile().exists())
+               {
+                       // photo file doesn't exist any more
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
+                       return false;
+               }
+               // Warn if file read-only and selected to overwrite
+               if (inOverwriteFlag && !inPhoto.getFile().canWrite())
+               {
+                       // eek, can't overwrite file
+                       int answer = JOptionPane.showConfirmDialog(_parentFrame,
+                               I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
+                                       + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
+                               I18nManager.getText("dialog.saveexif.title"),
+                               JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
+                       if (answer == JOptionPane.YES_OPTION)
+                       {
+                               // don't overwrite this image but write to copy
+                               inOverwriteFlag = false;
+                       }
+                       else
+                       {
+                               // don't do anything with this file
+                               return false;
+                       }
+               }
+               String[] command = null;
+               if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
+               {
+                       // Photo is no longer connected, so delete gps tags
+                       command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
+               }
+               else
+               {
+                       // Photo is now connected, so write new gps tags
+                       command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag, inForceFlag);
+               }
+               // Execute exif command
+               boolean saved = false;
+               try
+               {
+                       Process process = Runtime.getRuntime().exec(command);
+                       // Wait for process to finish so not too many run in parallel
+                       try {
+                               process.waitFor();
+                       }
+                       catch (InterruptedException ie) {}
+                       saved = (process.exitValue() == 0);
+               }
+               catch (Exception e)
+               {
+                       // show error message
+                       JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
+                               + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
+               }
+               return saved;
+       }
+
+
+       /**
+        * Create the command to delete the gps exif tags from the specified file
+        * @param inFile file from which to delete tags
+        * @param inOverwrite true to overwrite file, false to create copy
+        * @return external command to delete gps tags
+        */
+       private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
+       {
+               // Make a string array to construct the command and its parameters
+               String[] result = new String[inOverwrite?5:4];
+               result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
+               result[1] = "-P";
+               if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
+               // remove all gps tags
+               int paramOffset = inOverwrite?3:2;
+               result[paramOffset] = "-GPS:All=";
+               result[paramOffset + 1] = inFile.getAbsolutePath();
+               return result;
+       }
+
+
+       /**
+        * Create the comand to write the gps exif tags to the specified file
+        * @param inFile file to which to write the tags
+        * @param inPoint DataPoint object containing coordinate information
+        * @param inOverwrite true to overwrite file, false to create copy
+        * @param inForce true to force write, ignoring minor errors
+        * @return external command to write gps tags
+        */
+       private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint,
+               boolean inOverwrite, boolean inForce)
+       {
+               // Make a string array to construct the command and its parameters
+               String[] result = new String[(inOverwrite?10:9) + (inForce?1:0)];
+               result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
+               result[1] = "-P";
+               if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
+               int paramOffset = inOverwrite?3:2;
+               if (inForce) {
+                       result[paramOffset] = "-m";
+                       paramOffset++;
+               }
+               // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
+               // (latitude as space-separated deg min sec, reference as either N or S)
+               result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
+                + "'";
+               result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
+               // same for longitude with space-separated deg min sec, reference as either E or W
+               result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
+                + "'";
+               result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
+               // add altitude if it has it
+               result[paramOffset + 4] = "-GPSAltitude="
+                + (inPoint.hasAltitude()?inPoint.getAltitude().getMetricValue():0);
+               result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
+               // add the filename to modify
+               result[paramOffset + 6] = inFile.getAbsolutePath();
+               return result;
+       }
+}