]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ExifSaver.java
Version 11, August 2010
[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                 // Check whether photo file still exists
300                 if (!inPhoto.getFile().exists())
301                 {
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);
306                         return false;
307                 }
308                 // Warn if file read-only and selected to overwrite
309                 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
310                 {
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)
318                         {
319                                 // don't overwrite this image but write to copy
320                                 inOverwriteFlag = false;
321                         }
322                         else
323                         {
324                                 // don't do anything with this file
325                                 return false;
326                         }
327                 }
328                 String[] command = null;
329                 if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
330                 {
331                         // Photo is no longer connected, so delete gps tags
332                         command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
333                 }
334                 else
335                 {
336                         // Photo is now connected, so write new gps tags
337                         command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag, inForceFlag);
338                 }
339                 // Execute exif command
340                 boolean saved = false;
341                 try
342                 {
343                         Process process = Runtime.getRuntime().exec(command);
344                         // Wait for process to finish so not too many run in parallel
345                         try {
346                                 process.waitFor();
347                         }
348                         catch (InterruptedException ie) {}
349                         saved = (process.exitValue() == 0);
350                 }
351                 catch (Exception e)
352                 {
353                         // show error message
354                         JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
355                                 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
356                 }
357                 return saved;
358         }
359
360
361         /**
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
366          */
367         private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
368         {
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);
372                 result[1] = "-P";
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();
378                 return result;
379         }
380
381
382         /**
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
389          */
390         private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint,
391                 boolean inOverwrite, boolean inForce)
392         {
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);
396                 result[1] = "-P";
397                 if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
398                 int paramOffset = inOverwrite?3:2;
399                 if (inForce) {
400                         result[paramOffset] = "-m";
401                         paramOffset++;
402                 }
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)
406                  + "'";
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)
410                  + "'";
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();
418                 return result;
419         }
420 }