1 package tim.prune.save;
3 import java.awt.BorderLayout;
4 import java.awt.Dimension;
5 import java.awt.FlowLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
11 import javax.swing.BorderFactory;
12 import javax.swing.BoxLayout;
13 import javax.swing.JButton;
14 import javax.swing.JCheckBox;
15 import javax.swing.JDialog;
16 import javax.swing.JLabel;
17 import javax.swing.JOptionPane;
18 import javax.swing.JPanel;
19 import javax.swing.JProgressBar;
20 import javax.swing.JScrollPane;
21 import javax.swing.JTable;
23 import tim.prune.ExternalTools;
24 import tim.prune.I18nManager;
25 import tim.prune.UpdateMessageBroker;
26 import tim.prune.config.Config;
27 import tim.prune.data.Altitude;
28 import tim.prune.data.Coordinate;
29 import tim.prune.data.DataPoint;
30 import tim.prune.data.Photo;
31 import tim.prune.data.PhotoList;
34 * Class to call Exiftool to save coordinate information in jpg files
36 public class ExifSaver implements Runnable
38 private Frame _parentFrame = null;
39 private JDialog _dialog = null;
40 private JButton _okButton = null;
41 private JCheckBox _overwriteCheckbox = null;
42 private JCheckBox _forceCheckbox = null;
43 private JProgressBar _progressBar = null;
44 private PhotoTableModel _photoTableModel = null;
45 private boolean _saveCancelled = false;
48 // To preserve timestamps of file use parameter -P
49 // To overwrite file (careful!) use parameter -overwrite_original_in_place
51 // To read all GPS tags, use -GPS:All
52 // To delete all GPS tags, use -GPS:All=
54 // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
55 // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
57 // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
59 // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
60 // (setting altitude ref to 0 doesn't work)
61 // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
62 // (latitude as space-separated deg min sec, reference as either N or S)
63 // Same for longitude, reference E or W
68 * @param inParentFrame parent frame
70 public ExifSaver(Frame inParentFrame)
72 _parentFrame = inParentFrame;
77 * Save exif information to all photos in the list
78 * whose coordinate information has changed since loading
79 * @param inPhotoList list of photos to save
80 * @return true if saved
82 public boolean saveExifInformation(PhotoList inPhotoList)
84 // Check if external exif tool can be called
85 boolean exifToolInstalled = ExternalTools.isToolInstalled(ExternalTools.TOOL_EXIFTOOL);
86 if (!exifToolInstalled)
89 int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
90 I18nManager.getText("dialog.saveexif.title"),
91 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
92 if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
97 // Make model and add all photos to it
98 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
99 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
101 Photo photo = inPhotoList.getPhoto(i);
102 PhotoTableEntry entry = new PhotoTableEntry(photo);
103 _photoTableModel.addPhotoInfo(entry);
105 // Check if there are any modified photos to save
106 if (_photoTableModel.getNumSaveablePhotos() < 1)
108 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
109 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
113 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
114 _dialog.setLocationRelativeTo(_parentFrame);
115 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
116 _dialog.getContentPane().add(makeDialogComponents());
118 // set progress bar and show dialog
119 _progressBar.setVisible(false);
120 _dialog.setVisible(true);
126 * Put together the dialog components for adding to the gui
127 * @return panel containing all gui components
129 private JPanel makeDialogComponents()
131 JPanel panel = new JPanel();
132 panel.setLayout(new BorderLayout());
134 JLabel topLabel = new JLabel(I18nManager.getText("dialog.saveexif.intro"));
135 topLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
136 panel.add(topLabel, BorderLayout.NORTH);
137 // centre panel with most controls
138 JPanel centrePanel = new JPanel();
139 centrePanel.setLayout(new BorderLayout());
140 // table panel with table and checkbox
141 JPanel tablePanel = new JPanel();
142 tablePanel.setLayout(new BorderLayout());
143 JTable photoTable = new JTable(_photoTableModel);
144 JScrollPane scrollPane = new JScrollPane(photoTable);
145 scrollPane.setPreferredSize(new Dimension(300, 160));
146 tablePanel.add(scrollPane, BorderLayout.CENTER);
147 // Pair of checkboxes
148 JPanel checkPanel = new JPanel();
149 checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
150 _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
151 _overwriteCheckbox.setSelected(false);
152 checkPanel.add(_overwriteCheckbox);
153 _forceCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.force"));
154 _forceCheckbox.setSelected(false);
155 checkPanel.add(_forceCheckbox);
156 tablePanel.add(checkPanel, BorderLayout.SOUTH);
157 centrePanel.add(tablePanel, BorderLayout.CENTER);
158 // progress bar below main controls
159 _progressBar = new JProgressBar(0, 100);
160 centrePanel.add(_progressBar, BorderLayout.SOUTH);
161 panel.add(centrePanel, BorderLayout.CENTER);
162 // Right-hand panel with select all, none buttons
163 JPanel rightPanel = new JPanel();
164 rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
165 JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
166 selectAllButton.addActionListener(new ActionListener() {
167 public void actionPerformed(ActionEvent e)
172 rightPanel.add(selectAllButton);
173 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
174 selectNoneButton.addActionListener(new ActionListener() {
175 public void actionPerformed(ActionEvent e)
180 rightPanel.add(selectNoneButton);
181 panel.add(rightPanel, BorderLayout.EAST);
182 // Lower panel with ok and cancel buttons
183 JPanel buttonPanel = new JPanel();
184 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
185 _okButton = new JButton(I18nManager.getText("button.ok"));
186 _okButton.addActionListener(new ActionListener() {
187 public void actionPerformed(ActionEvent e)
190 _okButton.setEnabled(false);
191 // start new thread to do save
192 new Thread(ExifSaver.this).start();
195 buttonPanel.add(_okButton);
196 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
197 cancelButton.addActionListener(new ActionListener() {
198 public void actionPerformed(ActionEvent e)
200 _saveCancelled = true;
204 buttonPanel.add(cancelButton);
205 panel.add(buttonPanel, BorderLayout.SOUTH);
211 * Select all or select none
212 * @param inSelected true to select all photos, false to deselect all
214 private void selectPhotos(boolean inSelected)
216 int numPhotos = _photoTableModel.getRowCount();
217 for (int i=0; i<numPhotos; i++)
219 _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
221 _photoTableModel.fireTableDataChanged();
226 * Run method for saving in separate thread
230 _saveCancelled = false;
231 PhotoTableEntry entry = null;
233 int numPhotos = _photoTableModel.getRowCount();
234 _progressBar.setMaximum(numPhotos);
235 _progressBar.setValue(0);
236 _progressBar.setVisible(true);
237 boolean overwriteFlag = _overwriteCheckbox.isSelected();
238 int numSaved = 0, numFailed = 0, numForced = 0;
239 // Loop over all photos in list
240 for (int i=0; i<numPhotos; i++)
242 entry = _photoTableModel.getPhotoTableEntry(i);
243 if (entry != null && entry.getSaveFlag() && !_saveCancelled)
245 // Only look at photos which are selected and whose status has changed since load
246 photo = entry.getPhoto();
247 if (photo != null && photo.getOriginalStatus() != photo.getCurrentStatus())
249 // Increment counter if save successful
250 if (savePhoto(photo, overwriteFlag, false)) {
254 if (_forceCheckbox.isSelected() && savePhoto(photo, overwriteFlag, true))
264 // update progress bar
265 _progressBar.setValue(i + 1);
267 _progressBar.setVisible(false);
269 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.saveexif.ok1") + " "
270 + numSaved + " " + I18nManager.getText("confirm.saveexif.ok2"));
273 JOptionPane.showMessageDialog(_parentFrame,
274 I18nManager.getText("error.saveexif.failed1") + " " + numFailed + " "
275 + I18nManager.getText("error.saveexif.failed2"),
276 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
280 JOptionPane.showMessageDialog(_parentFrame,
281 I18nManager.getText("error.saveexif.forced1") + " " + numForced + " "
282 + I18nManager.getText("error.saveexif.forced2"),
283 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
285 // close dialog, all finished
291 * Save the details for the given photo
292 * @param inPhoto Photo object
293 * @param inOverwriteFlag true to overwrite file, false otherwise
294 * @param inForceFlag true to force write, ignoring minor errors
295 * @return true if details saved ok
297 private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag, boolean inForceFlag)
299 // Check whether photo file still exists
300 if (!inPhoto.getFile().exists())
302 // photo file doesn't exist any more
303 JOptionPane.showMessageDialog(_parentFrame,
304 I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
305 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
308 // Warn if file read-only and selected to overwrite
309 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
311 // eek, can't overwrite file
312 int answer = JOptionPane.showConfirmDialog(_parentFrame,
313 I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
314 + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
315 I18nManager.getText("dialog.saveexif.title"),
316 JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
317 if (answer == JOptionPane.YES_OPTION)
319 // don't overwrite this image but write to copy
320 inOverwriteFlag = false;
324 // don't do anything with this file
328 String[] command = null;
329 if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
331 // Photo is no longer connected, so delete gps tags
332 command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
336 // Photo is now connected, so write new gps tags
337 command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag, inForceFlag);
339 // Execute exif command
340 boolean saved = false;
343 Process process = Runtime.getRuntime().exec(command);
344 // Wait for process to finish so not too many run in parallel
348 catch (InterruptedException ie) {}
349 saved = (process.exitValue() == 0);
353 // show error message
354 JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
355 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
362 * Create the command to delete the gps exif tags from the specified file
363 * @param inFile file from which to delete tags
364 * @param inOverwrite true to overwrite file, false to create copy
365 * @return external command to delete gps tags
367 private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
369 // Make a string array to construct the command and its parameters
370 String[] result = new String[inOverwrite?5:4];
371 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
373 if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
374 // remove all gps tags
375 int paramOffset = inOverwrite?3:2;
376 result[paramOffset] = "-GPS:All=";
377 result[paramOffset + 1] = inFile.getAbsolutePath();
383 * Create the comand to write the gps exif tags to the specified file
384 * @param inFile file to which to write the tags
385 * @param inPoint DataPoint object containing coordinate information
386 * @param inOverwrite true to overwrite file, false to create copy
387 * @param inForce true to force write, ignoring minor errors
388 * @return external command to write gps tags
390 private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint,
391 boolean inOverwrite, boolean inForce)
393 // Make a string array to construct the command and its parameters
394 String[] result = new String[(inOverwrite?10:9) + (inForce?1:0)];
395 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
397 if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
398 int paramOffset = inOverwrite?3:2;
400 result[paramOffset] = "-m";
403 // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
404 // (latitude as space-separated deg min sec, reference as either N or S)
405 result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
407 result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
408 // same for longitude with space-separated deg min sec, reference as either E or W
409 result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
411 result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
412 // add altitude if it has it
413 result[paramOffset + 4] = "-GPSAltitude="
414 + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.Format.METRES):0);
415 result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
416 // add the filename to modify
417 result[paramOffset + 6] = inFile.getAbsolutePath();