]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ExifSaver.java
Version 5, May 2008
[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.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;
21
22 import tim.prune.ExternalTools;
23 import tim.prune.I18nManager;
24 import tim.prune.UpdateMessageBroker;
25 import tim.prune.data.Altitude;
26 import tim.prune.data.Coordinate;
27 import tim.prune.data.DataPoint;
28 import tim.prune.data.Photo;
29 import tim.prune.data.PhotoList;
30 import tim.prune.data.PhotoStatus;
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 JProgressBar _progressBar = null;
42         private PhotoTableModel _photoTableModel = null;
43         private boolean _saveCancelled = false;
44
45
46         // To preserve timestamps of file use parameter -P
47         // To overwrite file (careful!) use parameter -overwrite_original_in_place
48
49         // To read all GPS tags,   use -GPS:All
50         // To delete all GPS tags, use -GPS:All=
51
52         // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
53         // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
54
55         // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
56
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
62
63
64         /**
65          * Constructor
66          * @param inParentFrame parent frame
67          */
68         public ExifSaver(Frame inParentFrame)
69         {
70                 _parentFrame = inParentFrame;
71         }
72
73
74         /**
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
79          */
80         public boolean saveExifInformation(PhotoList inPhotoList)
81         {
82                 // Check if external exif tool can be called
83                 boolean exifToolInstalled = ExternalTools.isExiftoolInstalled();
84                 if (!exifToolInstalled)
85                 {
86                         // show warning
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)
91                         {
92                                 return false;
93                         }
94                 }
95                 // Make model and add all photos to it
96                 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
97                 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
98                 {
99                         Photo photo = inPhotoList.getPhoto(i);
100                         PhotoTableEntry entry = new PhotoTableEntry(photo);
101                         _photoTableModel.addPhotoInfo(entry);
102                 }
103                 // Check if there are any modified photos to save
104                 if (_photoTableModel.getNumSaveablePhotos() < 1)
105                 {
106                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
107                                 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
108                         return false;
109                 }
110                 // Construct dialog
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());
115                 _dialog.pack();
116                 // set progress bar and show dialog
117                 _progressBar.setVisible(false);
118                 _dialog.show();
119                 return true;
120         }
121
122
123         /**
124          * Put together the dialog components for adding to the gui
125          * @return panel containing all gui components
126          */
127         private JPanel makeDialogComponents()
128         {
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)
156                         {
157                                 selectPhotos(true);
158                         }
159                 });
160                 rightPanel.add(selectAllButton);
161                 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
162                 selectNoneButton.addActionListener(new ActionListener() {
163                         public void actionPerformed(ActionEvent e)
164                         {
165                                 selectPhotos(false);
166                         }
167                 });
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)
176                         {
177                                 // disable ok button
178                                 _okButton.setEnabled(false);
179                                 // start new thread to do save
180                                 new Thread(ExifSaver.this).start();
181                         }
182                 });
183                 buttonPanel.add(_okButton);
184                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
185                 cancelButton.addActionListener(new ActionListener() {
186                         public void actionPerformed(ActionEvent e)
187                         {
188                                 _saveCancelled = true;
189                                 _dialog.dispose();
190                         }
191                 });
192                 buttonPanel.add(cancelButton);
193                 panel.add(buttonPanel, BorderLayout.SOUTH);
194                 return panel;
195         }
196
197
198         /**
199          * Select all or select none
200          * @param inSelected true to select all photos, false to deselect all
201          */
202         private void selectPhotos(boolean inSelected)
203         {
204                 int numPhotos = _photoTableModel.getRowCount();
205                 for (int i=0; i<numPhotos; i++)
206                 {
207                         _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
208                 }
209                 _photoTableModel.fireTableDataChanged();
210         }
211
212
213         /**
214          * Run method for saving in separate thread
215          */
216         public void run()
217         {
218                 _saveCancelled = false;
219                 PhotoTableEntry entry = null;
220                 Photo photo = null;
221                 int numPhotos = _photoTableModel.getRowCount();
222                 _progressBar.setMaximum(numPhotos);
223                 _progressBar.setValue(0);
224                 _progressBar.setVisible(true);
225                 boolean overwriteFlag = _overwriteCheckbox.isSelected();
226                 int numSaved = 0;
227                 // Loop over all photos in list
228                 for (int i=0; i<numPhotos; i++)
229                 {
230                         entry = _photoTableModel.getPhotoTableEntry(i);
231                         if (entry != null && entry.getSaveFlag() && !_saveCancelled)
232                         {
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())
236                                 {
237                                         // Increment counter if save successful
238                                         if (savePhoto(photo, overwriteFlag))
239                                         {
240                                                 numSaved++;
241                                         }
242                                 }
243                         }
244                         // update progress bar
245                         _progressBar.setValue(i + 1);
246                 }
247                 _progressBar.setVisible(false);
248                 // Show confirmation
249                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.saveexif.ok1") + " "
250                         + numSaved + " " + I18nManager.getText("confirm.saveexif.ok2"));
251                 // close dialog, all finished
252                 _dialog.dispose();
253         }
254
255
256         /**
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
261          */
262         private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag)
263         {
264                 // Check whether photo file still exists
265                 if (!inPhoto.getFile().exists())
266                 {
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);
271                         return false;
272                 }
273                 // Warn if file read-only and selected to overwrite
274                 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
275                 {
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)
283                         {
284                                 // don't overwrite this image but write to copy
285                                 inOverwriteFlag = false;
286                         }
287                         else
288                         {
289                                 // don't do anything with this file
290                                 return false;
291                         }
292                 }
293                 String[] command = null;
294                 if (inPhoto.getCurrentStatus() == PhotoStatus.NOT_CONNECTED)
295                 {
296                         // Photo is no longer connected, so delete gps tags
297                         command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
298                 }
299                 else
300                 {
301                         // Photo is now connected, so write new gps tags
302                         command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag);
303                 }
304                 // Execute exif command
305                 try
306                 {
307                         Process process = Runtime.getRuntime().exec(command);
308                         // Wait for process to finish so not too many run in parallel
309                         try {
310                                 process.waitFor();
311                         }
312                         catch (InterruptedException ie) {}
313                 }
314                 catch (Exception e)
315                 {
316                         // show error message
317                         JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
318                                 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
319                         return false;
320                 }
321                 return true;
322         }
323
324
325         /**
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
330          */
331         private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
332         {
333                 // Make a string array to construct the command and its parameters
334                 String[] result = new String[inOverwrite?5:4];
335                 result[0] = "exiftool";
336                 result[1] = "-P";
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();
342                 return result;
343         }
344
345
346         /**
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
352          */
353         private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint, boolean inOverwrite)
354         {
355                 // Make a string array to construct the command and its parameters
356                 String[] result = new String[inOverwrite?10:9];
357                 result[0] = "exiftool";
358                 result[1] = "-P";
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)
364                  + "'";
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)
368                  + "'";
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();
376                 return result;
377         }
378 }