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.Coordinate;
28 import tim.prune.data.DataPoint;
29 import tim.prune.data.Photo;
30 import tim.prune.data.PhotoList;
33 * Class to call Exiftool to save coordinate information in jpg files
35 public class ExifSaver implements Runnable
37 private Frame _parentFrame = null;
38 private JDialog _dialog = null;
39 private JButton _okButton = null;
40 private JCheckBox _overwriteCheckbox = null;
41 private JCheckBox _forceCheckbox = null;
42 private JProgressBar _progressBar = null;
43 private PhotoTableModel _photoTableModel = null;
44 private boolean _saveCancelled = false;
47 // To preserve timestamps of file use parameter -P
48 // To overwrite file (careful!) use parameter -overwrite_original_in_place
50 // To read all GPS tags, use -GPS:All
51 // To delete all GPS tags, use -GPS:All=
53 // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
54 // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
56 // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
58 // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
59 // (setting altitude ref to 0 doesn't work)
60 // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
61 // (latitude as space-separated deg min sec, reference as either N or S)
62 // Same for longitude, reference E or W
67 * @param inParentFrame parent frame
69 public ExifSaver(Frame inParentFrame)
71 _parentFrame = inParentFrame;
76 * Save exif information to all photos in the list
77 * whose coordinate information has changed since loading
78 * @param inPhotoList list of photos to save
79 * @return true if saved
81 public boolean saveExifInformation(PhotoList inPhotoList)
83 // Check if external exif tool can be called
84 boolean exifToolInstalled = ExternalTools.isToolInstalled(ExternalTools.TOOL_EXIFTOOL);
85 if (!exifToolInstalled)
88 int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
89 I18nManager.getText("dialog.saveexif.title"),
90 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
91 if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
96 // Make model and add all photos to it
97 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
98 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
100 Photo photo = inPhotoList.getPhoto(i);
101 PhotoTableEntry entry = new PhotoTableEntry(photo);
102 _photoTableModel.addPhotoInfo(entry);
104 // Check if there are any modified photos to save
105 if (_photoTableModel.getNumSaveablePhotos() < 1)
107 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
108 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
112 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
113 _dialog.setLocationRelativeTo(_parentFrame);
114 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
115 _dialog.getContentPane().add(makeDialogComponents());
117 // set progress bar and show dialog
118 _progressBar.setVisible(false);
119 _dialog.setVisible(true);
125 * Put together the dialog components for adding to the gui
126 * @return panel containing all gui components
128 private JPanel makeDialogComponents()
130 JPanel panel = new JPanel();
131 panel.setLayout(new BorderLayout());
133 JLabel topLabel = new JLabel(I18nManager.getText("dialog.saveexif.intro"));
134 topLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
135 panel.add(topLabel, BorderLayout.NORTH);
136 // centre panel with most controls
137 JPanel centrePanel = new JPanel();
138 centrePanel.setLayout(new BorderLayout());
139 // table panel with table and checkbox
140 JPanel tablePanel = new JPanel();
141 tablePanel.setLayout(new BorderLayout());
142 JTable photoTable = new JTable(_photoTableModel);
143 JScrollPane scrollPane = new JScrollPane(photoTable);
144 scrollPane.setPreferredSize(new Dimension(300, 160));
145 tablePanel.add(scrollPane, BorderLayout.CENTER);
146 // Pair of checkboxes
147 JPanel checkPanel = new JPanel();
148 checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
149 _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
150 _overwriteCheckbox.setSelected(false);
151 checkPanel.add(_overwriteCheckbox);
152 _forceCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.force"));
153 _forceCheckbox.setSelected(false);
154 checkPanel.add(_forceCheckbox);
155 tablePanel.add(checkPanel, BorderLayout.SOUTH);
156 centrePanel.add(tablePanel, BorderLayout.CENTER);
157 // progress bar below main controls
158 _progressBar = new JProgressBar(0, 100);
159 centrePanel.add(_progressBar, BorderLayout.SOUTH);
160 panel.add(centrePanel, BorderLayout.CENTER);
161 // Right-hand panel with select all, none buttons
162 JPanel rightPanel = new JPanel();
163 rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
164 JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
165 selectAllButton.addActionListener(new ActionListener() {
166 public void actionPerformed(ActionEvent e)
171 rightPanel.add(selectAllButton);
172 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
173 selectNoneButton.addActionListener(new ActionListener() {
174 public void actionPerformed(ActionEvent e)
179 rightPanel.add(selectNoneButton);
180 panel.add(rightPanel, BorderLayout.EAST);
181 // Lower panel with ok and cancel buttons
182 JPanel buttonPanel = new JPanel();
183 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
184 _okButton = new JButton(I18nManager.getText("button.ok"));
185 _okButton.addActionListener(new ActionListener() {
186 public void actionPerformed(ActionEvent e)
189 _okButton.setEnabled(false);
190 // start new thread to do save
191 new Thread(ExifSaver.this).start();
194 buttonPanel.add(_okButton);
195 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
196 cancelButton.addActionListener(new ActionListener() {
197 public void actionPerformed(ActionEvent e)
199 _saveCancelled = true;
203 buttonPanel.add(cancelButton);
204 panel.add(buttonPanel, BorderLayout.SOUTH);
210 * Select all or select none
211 * @param inSelected true to select all photos, false to deselect all
213 private void selectPhotos(boolean inSelected)
215 int numPhotos = _photoTableModel.getRowCount();
216 for (int i=0; i<numPhotos; i++)
218 _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
220 _photoTableModel.fireTableDataChanged();
225 * Run method for saving in separate thread
229 _saveCancelled = false;
230 PhotoTableEntry entry = null;
232 int numPhotos = _photoTableModel.getRowCount();
233 _progressBar.setMaximum(numPhotos);
234 _progressBar.setValue(0);
235 _progressBar.setVisible(true);
236 boolean overwriteFlag = _overwriteCheckbox.isSelected();
237 int numSaved = 0, numFailed = 0, numForced = 0;
238 // Loop over all photos in list
239 for (int i=0; i<numPhotos; i++)
241 entry = _photoTableModel.getPhotoTableEntry(i);
242 if (entry != null && entry.getSaveFlag() && !_saveCancelled)
244 // Only look at photos which are selected and whose status has changed since load
245 photo = entry.getPhoto();
246 if (photo != null && photo.isModified())
248 // Increment counter if save successful
249 if (savePhoto(photo, overwriteFlag, false)) {
253 if (_forceCheckbox.isSelected() && savePhoto(photo, overwriteFlag, true))
263 // update progress bar
264 _progressBar.setValue(i + 1);
266 _progressBar.setVisible(false);
268 UpdateMessageBroker.informSubscribers(I18nManager.getTextWithNumber("confirm.saveexif.ok", numSaved));
271 JOptionPane.showMessageDialog(_parentFrame,
272 I18nManager.getTextWithNumber("error.saveexif.failed", numFailed),
273 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
277 JOptionPane.showMessageDialog(_parentFrame,
278 I18nManager.getTextWithNumber("error.saveexif.forced", numForced),
279 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
281 // close dialog, all finished
287 * Save the details for the given photo
288 * @param inPhoto Photo object
289 * @param inOverwriteFlag true to overwrite file, false otherwise
290 * @param inForceFlag true to force write, ignoring minor errors
291 * @return true if details saved ok
293 private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag, boolean inForceFlag)
295 // If photos don't have a file, then can't save them
296 if (inPhoto.getFile() == null) {
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().getMetricValue():0);
415 result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
416 // add the filename to modify
417 result[paramOffset + 6] = inFile.getAbsolutePath();