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.BoxLayout;
12 import javax.swing.JButton;
13 import javax.swing.JCheckBox;
14 import javax.swing.JDialog;
15 import javax.swing.JLabel;
16 import javax.swing.JOptionPane;
17 import javax.swing.JPanel;
18 import javax.swing.JProgressBar;
19 import javax.swing.JScrollPane;
20 import javax.swing.JTable;
22 import tim.prune.Config;
23 import tim.prune.ExternalTools;
24 import tim.prune.I18nManager;
25 import tim.prune.UpdateMessageBroker;
26 import tim.prune.data.Altitude;
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 JProgressBar _progressBar = null;
42 private PhotoTableModel _photoTableModel = null;
43 private boolean _saveCancelled = false;
46 // To preserve timestamps of file use parameter -P
47 // To overwrite file (careful!) use parameter -overwrite_original_in_place
49 // To read all GPS tags, use -GPS:All
50 // To delete all GPS tags, use -GPS:All=
52 // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
53 // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
55 // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
57 // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
58 // (setting altitude ref to 0 doesn't work)
59 // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
60 // (latitude as space-separated deg min sec, reference as either N or S)
61 // Same for longitude, reference E or W
66 * @param inParentFrame parent frame
68 public ExifSaver(Frame inParentFrame)
70 _parentFrame = inParentFrame;
75 * Save exif information to all photos in the list
76 * whose coordinate information has changed since loading
77 * @param inPhotoList list of photos to save
78 * @return true if saved
80 public boolean saveExifInformation(PhotoList inPhotoList)
82 // Check if external exif tool can be called
83 boolean exifToolInstalled = ExternalTools.isToolInstalled(ExternalTools.TOOL_EXIFTOOL);
84 if (!exifToolInstalled)
87 int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
88 I18nManager.getText("dialog.saveexif.title"),
89 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
90 if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
95 // Make model and add all photos to it
96 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
97 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
99 Photo photo = inPhotoList.getPhoto(i);
100 PhotoTableEntry entry = new PhotoTableEntry(photo);
101 _photoTableModel.addPhotoInfo(entry);
103 // Check if there are any modified photos to save
104 if (_photoTableModel.getNumSaveablePhotos() < 1)
106 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
107 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
111 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
112 _dialog.setLocationRelativeTo(_parentFrame);
113 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
114 _dialog.getContentPane().add(makeDialogComponents());
116 // set progress bar and show dialog
117 _progressBar.setVisible(false);
118 _dialog.setVisible(true);
124 * Put together the dialog components for adding to the gui
125 * @return panel containing all gui components
127 private JPanel makeDialogComponents()
129 JPanel panel = new JPanel();
130 panel.setLayout(new BorderLayout());
131 panel.add(new JLabel(I18nManager.getText("dialog.saveexif.intro")), BorderLayout.NORTH);
132 // centre panel with most controls
133 JPanel centrePanel = new JPanel();
134 centrePanel.setLayout(new BorderLayout());
135 // table panel with table and checkbox
136 JPanel tablePanel = new JPanel();
137 tablePanel.setLayout(new BorderLayout());
138 JTable photoTable = new JTable(_photoTableModel);
139 JScrollPane scrollPane = new JScrollPane(photoTable);
140 scrollPane.setPreferredSize(new Dimension(300, 160));
141 tablePanel.add(scrollPane, BorderLayout.CENTER);
142 _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
143 _overwriteCheckbox.setSelected(false);
144 tablePanel.add(_overwriteCheckbox, BorderLayout.SOUTH);
145 centrePanel.add(tablePanel, BorderLayout.CENTER);
146 // progress bar below main controls
147 _progressBar = new JProgressBar(0, 100);
148 centrePanel.add(_progressBar, BorderLayout.SOUTH);
149 panel.add(centrePanel, BorderLayout.CENTER);
150 // Right-hand panel with select all, none buttons
151 JPanel rightPanel = new JPanel();
152 rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
153 JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
154 selectAllButton.addActionListener(new ActionListener() {
155 public void actionPerformed(ActionEvent e)
160 rightPanel.add(selectAllButton);
161 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
162 selectNoneButton.addActionListener(new ActionListener() {
163 public void actionPerformed(ActionEvent e)
168 rightPanel.add(selectNoneButton);
169 panel.add(rightPanel, BorderLayout.EAST);
170 // Lower panel with ok and cancel buttons
171 JPanel buttonPanel = new JPanel();
172 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
173 _okButton = new JButton(I18nManager.getText("button.ok"));
174 _okButton.addActionListener(new ActionListener() {
175 public void actionPerformed(ActionEvent e)
178 _okButton.setEnabled(false);
179 // start new thread to do save
180 new Thread(ExifSaver.this).start();
183 buttonPanel.add(_okButton);
184 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
185 cancelButton.addActionListener(new ActionListener() {
186 public void actionPerformed(ActionEvent e)
188 _saveCancelled = true;
192 buttonPanel.add(cancelButton);
193 panel.add(buttonPanel, BorderLayout.SOUTH);
199 * Select all or select none
200 * @param inSelected true to select all photos, false to deselect all
202 private void selectPhotos(boolean inSelected)
204 int numPhotos = _photoTableModel.getRowCount();
205 for (int i=0; i<numPhotos; i++)
207 _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
209 _photoTableModel.fireTableDataChanged();
214 * Run method for saving in separate thread
218 _saveCancelled = false;
219 PhotoTableEntry entry = null;
221 int numPhotos = _photoTableModel.getRowCount();
222 _progressBar.setMaximum(numPhotos);
223 _progressBar.setValue(0);
224 _progressBar.setVisible(true);
225 boolean overwriteFlag = _overwriteCheckbox.isSelected();
227 // Loop over all photos in list
228 for (int i=0; i<numPhotos; i++)
230 entry = _photoTableModel.getPhotoTableEntry(i);
231 if (entry != null && entry.getSaveFlag() && !_saveCancelled)
233 // Only look at photos which are selected and whose status has changed since load
234 photo = entry.getPhoto();
235 if (photo != null && photo.getOriginalStatus() != photo.getCurrentStatus())
237 // Increment counter if save successful
238 if (savePhoto(photo, overwriteFlag))
244 // update progress bar
245 _progressBar.setValue(i + 1);
247 _progressBar.setVisible(false);
249 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.saveexif.ok1") + " "
250 + numSaved + " " + I18nManager.getText("confirm.saveexif.ok2"));
251 // close dialog, all finished
257 * Save the details for the given photo
258 * @param inPhoto Photo object
259 * @param inOverwriteFlag true to overwrite file, false otherwise
260 * @return true if details saved ok
262 private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag)
264 // Check whether photo file still exists
265 if (!inPhoto.getFile().exists())
267 // photo file doesn't exist any more
268 JOptionPane.showMessageDialog(_parentFrame,
269 I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
270 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
273 // Warn if file read-only and selected to overwrite
274 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
276 // eek, can't overwrite file
277 int answer = JOptionPane.showConfirmDialog(_parentFrame,
278 I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
279 + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
280 I18nManager.getText("dialog.saveexif.title"),
281 JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
282 if (answer == JOptionPane.YES_OPTION)
284 // don't overwrite this image but write to copy
285 inOverwriteFlag = false;
289 // don't do anything with this file
293 String[] command = null;
294 if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
296 // Photo is no longer connected, so delete gps tags
297 command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
301 // Photo is now connected, so write new gps tags
302 command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag);
304 // Execute exif command
307 Process process = Runtime.getRuntime().exec(command);
308 // Wait for process to finish so not too many run in parallel
312 catch (InterruptedException ie) {}
316 // show error message
317 JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
318 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
326 * Create the command to delete the gps exif tags from the specified file
327 * @param inFile file from which to delete tags
328 * @param inOverwrite true to overwrite file, false to create copy
329 * @return external command to delete gps tags
331 private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
333 // Make a string array to construct the command and its parameters
334 String[] result = new String[inOverwrite?5:4];
335 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
337 if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
338 // remove all gps tags
339 int paramOffset = inOverwrite?3:2;
340 result[paramOffset] = "-GPS:All=";
341 result[paramOffset + 1] = inFile.getAbsolutePath();
347 * Create the comand to write the gps exif tags to the specified file
348 * @param inFile file to which to write the tags
349 * @param inPoint DataPoint object containing coordinate information
350 * @param inOverwrite true to overwrite file, false to create copy
351 * @return external command to write gps tags
353 private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint, boolean inOverwrite)
355 // Make a string array to construct the command and its parameters
356 String[] result = new String[inOverwrite?10:9];
357 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
359 if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
360 int paramOffset = inOverwrite?3:2;
361 // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
362 // (latitude as space-separated deg min sec, reference as either N or S)
363 result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
365 result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
366 // same for longitude with space-separated deg min sec, reference as either E or W
367 result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
369 result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
370 // add altitude if it has it
371 result[paramOffset + 4] = "-GPSAltitude="
372 + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.Format.METRES):0);
373 result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
374 // add the filename to modify
375 result[paramOffset + 6] = inFile.getAbsolutePath();