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; } }