]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ExifSaver.java
Version 13, August 2011
[GpsPrune.git] / tim / prune / save / ExifSaver.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Dimension;
5 import java.awt.FlowLayout;
6 import java.awt.Frame;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.io.File;
10
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;
22
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;
32
33 /**
34  * Class to call Exiftool to save coordinate information in jpg files
35  */
36 public class ExifSaver implements Runnable
37 {
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;
46
47
48         // To preserve timestamps of file use parameter -P
49         // To overwrite file (careful!) use parameter -overwrite_original_in_place
50
51         // To read all GPS tags,   use -GPS:All
52         // To delete all GPS tags, use -GPS:All=
53
54         // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
55         // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
56
57         // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
58
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
64
65
66         /**
67          * Constructor
68          * @param inParentFrame parent frame
69          */
70         public ExifSaver(Frame inParentFrame)
71         {
72                 _parentFrame = inParentFrame;
73         }
74
75
76         /**
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
81          */
82         public boolean saveExifInformation(PhotoList inPhotoList)
83         {
84                 // Check if external exif tool can be called
85                 boolean exifToolInstalled = ExternalTools.isToolInstalled(ExternalTools.TOOL_EXIFTOOL);
86                 if (!exifToolInstalled)
87                 {
88                         // show warning
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)
93                         {
94                                 return false;
95                         }
96                 }
97                 // Make model and add all photos to it
98                 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
99                 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
100                 {
101                         Photo photo = inPhotoList.getPhoto(i);
102                         PhotoTableEntry entry = new PhotoTableEntry(photo);
103                         _photoTableModel.addPhotoInfo(entry);
104                 }
105                 // Check if there are any modified photos to save
106                 if (_photoTableModel.getNumSaveablePhotos() < 1)
107                 {
108                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
109                                 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
110                         return false;
111                 }
112                 // Construct dialog
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());
117                 _dialog.pack();
118                 // set progress bar and show dialog
119                 _progressBar.setVisible(false);
120                 _dialog.setVisible(true);
121                 return true;
122         }
123
124
125         /**
126          * Put together the dialog components for adding to the gui
127          * @return panel containing all gui components
128          */
129         private JPanel makeDialogComponents()
130         {
131                 JPanel panel = new JPanel();
132                 panel.setLayout(new BorderLayout());
133                 // Label at top
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)
168                         {
169                                 selectPhotos(true);
170                         }
171                 });
172                 rightPanel.add(selectAllButton);
173                 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
174                 selectNoneButton.addActionListener(new ActionListener() {
175                         public void actionPerformed(ActionEvent e)
176                         {
177                                 selectPhotos(false);
178                         }
179                 });
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)
188                         {
189                                 // disable ok button
190                                 _okButton.setEnabled(false);
191                                 // start new thread to do save
192                                 new Thread(ExifSaver.this).start();
193                         }
194                 });
195                 buttonPanel.add(_okButton);
196                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
197                 cancelButton.addActionListener(new ActionListener() {
198                         public void actionPerformed(ActionEvent e)
199                         {
200                                 _saveCancelled = true;
201                                 _dialog.dispose();
202                         }
203                 });
204                 buttonPanel.add(cancelButton);
205                 panel.add(buttonPanel, BorderLayout.SOUTH);
206                 return panel;
207         }
208
209
210         /**
211          * Select all or select none
212          * @param inSelected true to select all photos, false to deselect all
213          */
214         private void selectPhotos(boolean inSelected)
215         {
216                 int numPhotos = _photoTableModel.getRowCount();
217                 for (int i=0; i<numPhotos; i++)
218                 {
219                         _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
220                 }
221                 _photoTableModel.fireTableDataChanged();
222         }
223
224
225         /**
226          * Run method for saving in separate thread
227          */
228         public void run()
229         {
230                 _saveCancelled = false;
231                 PhotoTableEntry entry = null;
232                 Photo photo = 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++)
241                 {
242                         entry = _photoTableModel.getPhotoTableEntry(i);
243                         if (entry != null && entry.getSaveFlag() && !_saveCancelled)
244                         {
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())
248                                 {
249                                         // Increment counter if save successful
250                                         if (savePhoto(photo, overwriteFlag, false)) {
251                                                 numSaved++;
252                                         }
253                                         else {
254                                                 if (_forceCheckbox.isSelected() && savePhoto(photo, overwriteFlag, true))
255                                                 {
256                                                         numForced++;
257                                                 }
258                                                 else {
259                                                         numFailed++;
260                                                 }
261                                         }
262                                 }
263                         }
264                         // update progress bar
265                         _progressBar.setValue(i + 1);
266                 }
267                 _progressBar.setVisible(false);
268                 // Show confirmation
269                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.saveexif.ok1") + " "
270                         + numSaved + " " + I18nManager.getText("confirm.saveexif.ok2"));
271                 if (numFailed > 0)
272                 {
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);
277                 }
278                 if (numForced > 0)
279                 {
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);
284                 }
285                 // close dialog, all finished
286                 _dialog.dispose();
287         }
288
289
290         /**
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
296          */
297         private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag, boolean inForceFlag)
298         {
299                 // If photos don't have a file, then can't save them
300                 if (inPhoto.getFile() == null) {
301                         return false;
302                 }
303                 // Check whether photo file still exists
304                 if (!inPhoto.getFile().exists())
305                 {
306                         // photo file doesn't exist any more
307                         JOptionPane.showMessageDialog(_parentFrame,
308                                 I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
309                                 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
310                         return false;
311                 }
312                 // Warn if file read-only and selected to overwrite
313                 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
314                 {
315                         // eek, can't overwrite file
316                         int answer = JOptionPane.showConfirmDialog(_parentFrame,
317                                 I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
318                                         + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
319                                 I18nManager.getText("dialog.saveexif.title"),
320                                 JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
321                         if (answer == JOptionPane.YES_OPTION)
322                         {
323                                 // don't overwrite this image but write to copy
324                                 inOverwriteFlag = false;
325                         }
326                         else
327                         {
328                                 // don't do anything with this file
329                                 return false;
330                         }
331                 }
332                 String[] command = null;
333                 if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
334                 {
335                         // Photo is no longer connected, so delete gps tags
336                         command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
337                 }
338                 else
339                 {
340                         // Photo is now connected, so write new gps tags
341                         command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag, inForceFlag);
342                 }
343                 // Execute exif command
344                 boolean saved = false;
345                 try
346                 {
347                         Process process = Runtime.getRuntime().exec(command);
348                         // Wait for process to finish so not too many run in parallel
349                         try {
350                                 process.waitFor();
351                         }
352                         catch (InterruptedException ie) {}
353                         saved = (process.exitValue() == 0);
354                 }
355                 catch (Exception e)
356                 {
357                         // show error message
358                         JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
359                                 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
360                 }
361                 return saved;
362         }
363
364
365         /**
366          * Create the command to delete the gps exif tags from the specified file
367          * @param inFile file from which to delete tags
368          * @param inOverwrite true to overwrite file, false to create copy
369          * @return external command to delete gps tags
370          */
371         private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
372         {
373                 // Make a string array to construct the command and its parameters
374                 String[] result = new String[inOverwrite?5:4];
375                 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
376                 result[1] = "-P";
377                 if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
378                 // remove all gps tags
379                 int paramOffset = inOverwrite?3:2;
380                 result[paramOffset] = "-GPS:All=";
381                 result[paramOffset + 1] = inFile.getAbsolutePath();
382                 return result;
383         }
384
385
386         /**
387          * Create the comand to write the gps exif tags to the specified file
388          * @param inFile file to which to write the tags
389          * @param inPoint DataPoint object containing coordinate information
390          * @param inOverwrite true to overwrite file, false to create copy
391          * @param inForce true to force write, ignoring minor errors
392          * @return external command to write gps tags
393          */
394         private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint,
395                 boolean inOverwrite, boolean inForce)
396         {
397                 // Make a string array to construct the command and its parameters
398                 String[] result = new String[(inOverwrite?10:9) + (inForce?1:0)];
399                 result[0] = Config.getConfigString(Config.KEY_EXIFTOOL_PATH);
400                 result[1] = "-P";
401                 if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
402                 int paramOffset = inOverwrite?3:2;
403                 if (inForce) {
404                         result[paramOffset] = "-m";
405                         paramOffset++;
406                 }
407                 // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
408                 // (latitude as space-separated deg min sec, reference as either N or S)
409                 result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
410                  + "'";
411                 result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
412                 // same for longitude with space-separated deg min sec, reference as either E or W
413                 result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
414                  + "'";
415                 result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
416                 // add altitude if it has it
417                 result[paramOffset + 4] = "-GPSAltitude="
418                  + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.Format.METRES):0);
419                 result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
420                 // add the filename to modify
421                 result[paramOffset + 6] = inFile.getAbsolutePath();
422                 return result;
423         }
424 }