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