]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ExifSaver.java
6533fcbed0e39f3043bde1bca12cf22c762374f2
[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
31 /**
32  * Class to call Exiftool to save coordinate information in jpg files
33  */
34 public class ExifSaver implements Runnable
35 {
36         private Frame _parentFrame = null;
37         private JDialog _dialog = null;
38         private JButton _okButton = null;
39         private JCheckBox _overwriteCheckbox = null;
40         private JProgressBar _progressBar = null;
41         private PhotoTableModel _photoTableModel = null;
42         private boolean _saveCancelled = false;
43
44
45         // To preserve timestamps of file use parameter -P
46         // To overwrite file (careful!) use parameter -overwrite_original_in_place
47
48         // To read all GPS tags,   use -GPS:All
49         // To delete all GPS tags, use -GPS:All=
50
51         // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
52         // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
53
54         // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
55
56         // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
57         // (setting altitude ref to 0 doesn't work)
58         // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
59         // (latitude as space-separated deg min sec, reference as either N or S)
60         // Same for longitude, reference E or W
61
62
63         /**
64          * Constructor
65          * @param inParentFrame parent frame
66          */
67         public ExifSaver(Frame inParentFrame)
68         {
69                 _parentFrame = inParentFrame;
70         }
71
72
73         /**
74          * Save exif information to all photos in the list
75          * whose coordinate information has changed since loading
76          * @param inPhotoList list of photos to save
77          * @return true if saved
78          */
79         public boolean saveExifInformation(PhotoList inPhotoList)
80         {
81                 // Check if external exif tool can be called
82                 boolean exifToolInstalled = ExternalTools.isExiftoolInstalled();
83                 if (!exifToolInstalled)
84                 {
85                         // show warning
86                         int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
87                                 I18nManager.getText("dialog.saveexif.title"),
88                                 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
89                         if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
90                         {
91                                 return false;
92                         }
93                 }
94                 // Make model and add all photos to it
95                 _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
96                 for (int i=0; i<inPhotoList.getNumPhotos(); i++)
97                 {
98                         Photo photo = inPhotoList.getPhoto(i);
99                         PhotoTableEntry entry = new PhotoTableEntry(photo);
100                         _photoTableModel.addPhotoInfo(entry);
101                 }
102                 // Check if there are any modified photos to save
103                 if (_photoTableModel.getNumSaveablePhotos() < 1)
104                 {
105                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
106                                 I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
107                         return false;
108                 }
109                 // Construct dialog
110                 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
111                 _dialog.setLocationRelativeTo(_parentFrame);
112                 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
113                 _dialog.getContentPane().add(makeDialogComponents());
114                 _dialog.pack();
115                 // set progress bar and show dialog
116                 _progressBar.setVisible(false);
117                 _dialog.setVisible(true);
118                 return true;
119         }
120
121
122         /**
123          * Put together the dialog components for adding to the gui
124          * @return panel containing all gui components
125          */
126         private JPanel makeDialogComponents()
127         {
128                 JPanel panel = new JPanel();
129                 panel.setLayout(new BorderLayout());
130                 panel.add(new JLabel(I18nManager.getText("dialog.saveexif.intro")), BorderLayout.NORTH);
131                 // centre panel with most controls
132                 JPanel centrePanel = new JPanel();
133                 centrePanel.setLayout(new BorderLayout());
134                 // table panel with table and checkbox
135                 JPanel tablePanel = new JPanel();
136                 tablePanel.setLayout(new BorderLayout());
137                 JTable photoTable = new JTable(_photoTableModel);
138                 JScrollPane scrollPane = new JScrollPane(photoTable);
139                 scrollPane.setPreferredSize(new Dimension(300, 160));
140                 tablePanel.add(scrollPane, BorderLayout.CENTER);
141                 _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
142                 _overwriteCheckbox.setSelected(false);
143                 tablePanel.add(_overwriteCheckbox, BorderLayout.SOUTH);
144                 centrePanel.add(tablePanel, BorderLayout.CENTER);
145                 // progress bar below main controls
146                 _progressBar = new JProgressBar(0, 100);
147                 centrePanel.add(_progressBar, BorderLayout.SOUTH);
148                 panel.add(centrePanel, BorderLayout.CENTER);
149                 // Right-hand panel with select all, none buttons
150                 JPanel rightPanel = new JPanel();
151                 rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
152                 JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
153                 selectAllButton.addActionListener(new ActionListener() {
154                         public void actionPerformed(ActionEvent e)
155                         {
156                                 selectPhotos(true);
157                         }
158                 });
159                 rightPanel.add(selectAllButton);
160                 JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
161                 selectNoneButton.addActionListener(new ActionListener() {
162                         public void actionPerformed(ActionEvent e)
163                         {
164                                 selectPhotos(false);
165                         }
166                 });
167                 rightPanel.add(selectNoneButton);
168                 panel.add(rightPanel, BorderLayout.EAST);
169                 // Lower panel with ok and cancel buttons
170                 JPanel buttonPanel = new JPanel();
171                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
172                 _okButton = new JButton(I18nManager.getText("button.ok"));
173                 _okButton.addActionListener(new ActionListener() {
174                         public void actionPerformed(ActionEvent e)
175                         {
176                                 // disable ok button
177                                 _okButton.setEnabled(false);
178                                 // start new thread to do save
179                                 new Thread(ExifSaver.this).start();
180                         }
181                 });
182                 buttonPanel.add(_okButton);
183                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
184                 cancelButton.addActionListener(new ActionListener() {
185                         public void actionPerformed(ActionEvent e)
186                         {
187                                 _saveCancelled = true;
188                                 _dialog.dispose();
189                         }
190                 });
191                 buttonPanel.add(cancelButton);
192                 panel.add(buttonPanel, BorderLayout.SOUTH);
193                 return panel;
194         }
195
196
197         /**
198          * Select all or select none
199          * @param inSelected true to select all photos, false to deselect all
200          */
201         private void selectPhotos(boolean inSelected)
202         {
203                 int numPhotos = _photoTableModel.getRowCount();
204                 for (int i=0; i<numPhotos; i++)
205                 {
206                         _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
207                 }
208                 _photoTableModel.fireTableDataChanged();
209         }
210
211
212         /**
213          * Run method for saving in separate thread
214          */
215         public void run()
216         {
217                 _saveCancelled = false;
218                 PhotoTableEntry entry = null;
219                 Photo photo = null;
220                 int numPhotos = _photoTableModel.getRowCount();
221                 _progressBar.setMaximum(numPhotos);
222                 _progressBar.setValue(0);
223                 _progressBar.setVisible(true);
224                 boolean overwriteFlag = _overwriteCheckbox.isSelected();
225                 int numSaved = 0;
226                 // Loop over all photos in list
227                 for (int i=0; i<numPhotos; i++)
228                 {
229                         entry = _photoTableModel.getPhotoTableEntry(i);
230                         if (entry != null && entry.getSaveFlag() && !_saveCancelled)
231                         {
232                                 // Only look at photos which are selected and whose status has changed since load
233                                 photo = entry.getPhoto();
234                                 if (photo != null && photo.getOriginalStatus() != photo.getCurrentStatus())
235                                 {
236                                         // Increment counter if save successful
237                                         if (savePhoto(photo, overwriteFlag))
238                                         {
239                                                 numSaved++;
240                                         }
241                                 }
242                         }
243                         // update progress bar
244                         _progressBar.setValue(i + 1);
245                 }
246                 _progressBar.setVisible(false);
247                 // Show confirmation
248                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.saveexif.ok1") + " "
249                         + numSaved + " " + I18nManager.getText("confirm.saveexif.ok2"));
250                 // close dialog, all finished
251                 _dialog.dispose();
252         }
253
254
255         /**
256          * Save the details for the given photo
257          * @param inPhoto Photo object
258          * @param inOverwriteFlag true to overwrite file, false otherwise
259          * @return true if details saved ok
260          */
261         private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag)
262         {
263                 // Check whether photo file still exists
264                 if (!inPhoto.getFile().exists())
265                 {
266                         // photo file doesn't exist any more
267                         JOptionPane.showMessageDialog(_parentFrame,
268                                 I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
269                                 I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
270                         return false;
271                 }
272                 // Warn if file read-only and selected to overwrite
273                 if (inOverwriteFlag && !inPhoto.getFile().canWrite())
274                 {
275                         // eek, can't overwrite file
276                         int answer = JOptionPane.showConfirmDialog(_parentFrame,
277                                 I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
278                                         + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
279                                 I18nManager.getText("dialog.saveexif.title"),
280                                 JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
281                         if (answer == JOptionPane.YES_OPTION)
282                         {
283                                 // don't overwrite this image but write to copy
284                                 inOverwriteFlag = false;
285                         }
286                         else
287                         {
288                                 // don't do anything with this file
289                                 return false;
290                         }
291                 }
292                 String[] command = null;
293                 if (inPhoto.getCurrentStatus() == Photo.Status.NOT_CONNECTED)
294                 {
295                         // Photo is no longer connected, so delete gps tags
296                         command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
297                 }
298                 else
299                 {
300                         // Photo is now connected, so write new gps tags
301                         command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag);
302                 }
303                 // Execute exif command
304                 try
305                 {
306                         Process process = Runtime.getRuntime().exec(command);
307                         // Wait for process to finish so not too many run in parallel
308                         try {
309                                 process.waitFor();
310                         }
311                         catch (InterruptedException ie) {}
312                 }
313                 catch (Exception e)
314                 {
315                         // show error message
316                         JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
317                                 + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
318                         return false;
319                 }
320                 return true;
321         }
322
323
324         /**
325          * Create the command to delete the gps exif tags from the specified file
326          * @param inFile file from which to delete tags
327          * @param inOverwrite true to overwrite file, false to create copy
328          * @return external command to delete gps tags
329          */
330         private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
331         {
332                 // Make a string array to construct the command and its parameters
333                 String[] result = new String[inOverwrite?5:4];
334                 result[0] = "exiftool";
335                 result[1] = "-P";
336                 if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
337                 // remove all gps tags
338                 int paramOffset = inOverwrite?3:2;
339                 result[paramOffset] = "-GPS:All=";
340                 result[paramOffset + 1] = inFile.getAbsolutePath();
341                 return result;
342         }
343
344
345         /**
346          * Create the comand to write the gps exif tags to the specified file
347          * @param inFile file to which to write the tags
348          * @param inPoint DataPoint object containing coordinate information
349          * @param inOverwrite true to overwrite file, false to create copy
350          * @return external command to write gps tags
351          */
352         private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint, boolean inOverwrite)
353         {
354                 // Make a string array to construct the command and its parameters
355                 String[] result = new String[inOverwrite?10:9];
356                 result[0] = "exiftool";
357                 result[1] = "-P";
358                 if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
359                 int paramOffset = inOverwrite?3:2;
360                 // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
361                 // (latitude as space-separated deg min sec, reference as either N or S)
362                 result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
363                  + "'";
364                 result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
365                 // same for longitude with space-separated deg min sec, reference as either E or W
366                 result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
367                  + "'";
368                 result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
369                 // add altitude if it has it
370                 result[paramOffset + 4] = "-GPSAltitude="
371                  + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.Format.METRES):0);
372                 result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
373                 // add the filename to modify
374                 result[paramOffset + 6] = inFile.getAbsolutePath();
375                 return result;
376         }
377 }