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