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