]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/load/JpegLoader.java
Version 20.4, May 2021
[GpsPrune.git] / src / tim / prune / load / JpegLoader.java
1 package tim.prune.load;
2
3 import java.io.File;
4 import java.util.TreeSet;
5
6 import javax.swing.BoxLayout;
7 import javax.swing.JCheckBox;
8 import javax.swing.JFileChooser;
9 import javax.swing.JFrame;
10 import javax.swing.JPanel;
11
12 import tim.prune.App;
13 import tim.prune.I18nManager;
14 import tim.prune.config.Config;
15 import tim.prune.data.Altitude;
16 import tim.prune.data.DataPoint;
17 import tim.prune.data.LatLonRectangle;
18 import tim.prune.data.Latitude;
19 import tim.prune.data.Longitude;
20 import tim.prune.data.Photo;
21 import tim.prune.data.Timestamp;
22 import tim.prune.data.TimestampLocal;
23 import tim.prune.data.TimestampUtc;
24 import tim.prune.data.UnitSetLibrary;
25 import tim.prune.function.Cancellable;
26 import tim.prune.jpeg.InternalExifLibrary;
27 import tim.prune.jpeg.JpegData;
28
29 /**
30  * Class to manage the loading of Jpegs and dealing with the GPS data from them
31  */
32 public class JpegLoader implements Runnable, Cancellable
33 {
34         private App _app = null;
35         private JFrame _parentFrame = null;
36         private JFileChooser _fileChooser = null;
37         private GenericFileFilter _fileFilter = null;
38         private JCheckBox _subdirCheckbox = null;
39         private JCheckBox _noExifCheckbox = null;
40         private JCheckBox _outsideAreaCheckbox = null;
41         private MediaLoadProgressDialog _progressDialog = null;
42         private int[] _fileCounts = null;
43         private boolean _cancelled = false;
44         private LatLonRectangle _trackRectangle = null;
45         private TreeSet<Photo> _photos = null;
46
47
48         /**
49          * Constructor
50          * @param inApp Application object to inform of photo load
51          * @param inParentFrame parent frame to reference for dialogs
52          */
53         public JpegLoader(App inApp, JFrame inParentFrame)
54         {
55                 _app = inApp;
56                 _parentFrame = inParentFrame;
57                 _fileFilter = new JpegFileFilter();
58         }
59
60
61         /**
62          * Open the GUI to select options and start the load
63          * @param inRectangle track rectangle
64          */
65         public void openDialog(LatLonRectangle inRectangle)
66         {
67                 // Create file chooser if necessary
68                 if (_fileChooser == null)
69                 {
70                         _fileChooser = new JFileChooser();
71                         _fileChooser.setMultiSelectionEnabled(true);
72                         _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
73                         _fileChooser.setFileFilter(_fileFilter);
74                         _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
75                         _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
76                         _subdirCheckbox.setSelected(true);
77                         _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
78                         _noExifCheckbox.setSelected(true);
79                         _outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
80                         _outsideAreaCheckbox.setSelected(true);
81                         JPanel panel = new JPanel();
82                         panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
83                         panel.add(_subdirCheckbox);
84                         panel.add(_noExifCheckbox);
85                         panel.add(_outsideAreaCheckbox);
86                         _fileChooser.setAccessory(panel);
87                         // start from directory in config if already set by other operations
88                         String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
89                         if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
90                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
91                 }
92                 // enable/disable track checkbox
93                 _trackRectangle = inRectangle;
94                 _outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
95                 // Show file dialog to choose file / directory(ies)
96                 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
97                 {
98                         // Bring up dialog before starting
99                         _progressDialog = new MediaLoadProgressDialog(_parentFrame, this);
100                         _progressDialog.show();
101                         // start thread for processing
102                         new Thread(this).start();
103                 }
104         }
105
106         /** Cancel */
107         public void cancel() {
108                 _cancelled = true;
109         }
110
111
112         /**
113          * Run method for performing tasks in separate thread
114          */
115         public void run()
116         {
117                 // Initialise arrays, errors, summaries
118                 _fileCounts = new int[3]; // files, jpegs, gps
119                 _photos = new TreeSet<Photo>(new MediaSorter());
120                 File[] files = _fileChooser.getSelectedFiles();
121                 // Loop recursively over selected files/directories to count files
122                 int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
123                 // Set up the progress bar for this number of files
124                 _progressDialog.showProgress(0, numFiles);
125                 _cancelled = false;
126
127                 // Process the files recursively and build lists of photos
128                 processFileList(files, true, _subdirCheckbox.isSelected());
129                 _progressDialog.close();
130                 if (_cancelled) {return;}
131
132                 if (_fileCounts[0] == 0)
133                 {
134                         // No files found at all
135                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
136                 }
137                 else if (_fileCounts[1] == 0)
138                 {
139                         // No jpegs found
140                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
141                 }
142                 else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
143                 {
144                         // Need coordinates but no gps information found
145                         _app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
146                 }
147                 else
148                 {
149                         // Found some photos to load - pass information back to app
150                         _app.informPhotosLoaded(_photos);
151                 }
152         }
153
154
155         /**
156          * Process a list of files and/or directories
157          * @param inFiles array of file/directories
158          * @param inFirstDir true if first directory
159          * @param inDescend true to descend to subdirectories
160          */
161         private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
162         {
163                 if (inFiles == null) return;
164                 // Loop over elements in array
165                 for (int i=0; i<inFiles.length && !_cancelled; i++)
166                 {
167                         File file = inFiles[i];
168                         if (file.exists() && file.canRead())
169                         {
170                                 // Check whether it's a file or a directory
171                                 if (file.isFile())
172                                 {
173                                         processFile(file);
174                                 }
175                                 else if (file.isDirectory() && (inFirstDir || inDescend))
176                                 {
177                                         // Always process first directory,
178                                         // only process subdirectories if checkbox selected
179                                         File[] files = file.listFiles();
180                                         processFileList(files, false, inDescend);
181                                 }
182                         }
183                         // if file doesn't exist or isn't readable - ignore
184                 }
185         }
186
187
188         /**
189          * Process the given file, by attempting to extract its tags
190          * @param inFile file object to read
191          */
192         private void processFile(File inFile)
193         {
194                 // Update progress bar
195                 _fileCounts[0]++; // file found
196                 _progressDialog.showProgress(_fileCounts[0], -1);
197
198                 // Check whether filename corresponds with accepted filenames
199                 if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
200                 // If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
201
202                 if (inFile.exists() && inFile.canRead()) {
203                         _fileCounts[1]++; // jpeg found
204                 }
205                 Photo photo = createPhoto(inFile);
206                 if (photo.getDataPoint() != null) {
207                         _fileCounts[2]++; // photo has coordinates
208                 }
209                 // Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
210                 if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
211                         && (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
212                                 || _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
213                 {
214                         _photos.add(photo);
215                 }
216         }
217
218         /**
219          * Create a Photo object for the given file, including reading exif information
220          * @param inFile file object
221          * @return Photo object
222          */
223         public static Photo createPhoto(File inFile)
224         {
225                 // Create Photo object
226                 Photo photo = new Photo(inFile);
227                 // Try to get information out of exif
228                 JpegData jpegData = new InternalExifLibrary().getJpegData(inFile);
229                 Timestamp timestamp = null;
230                 if (jpegData != null)
231                 {
232                         if (jpegData.isGpsValid())
233                         {
234                                 timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
235                                 // Make DataPoint and attach to Photo
236                                 DataPoint point = createDataPoint(jpegData);
237                                 point.setPhoto(photo);
238                                 point.setSegmentStart(true);
239                                 photo.setDataPoint(point);
240                                 photo.setOriginalStatus(Photo.Status.TAGGED);
241                         }
242                         // Use exif timestamp if gps timestamp not available
243                         if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
244                                 timestamp = createTimestamp(jpegData.getOriginalTimestamp());
245                         }
246                         if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
247                                 timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
248                         }
249                         photo.setExifThumbnail(jpegData.getThumbnailImage());
250                         // Also extract orientation tag for setting rotation state of photo
251                         photo.setRotation(jpegData.getRequiredRotation());
252                         // Set bearing, if any
253                         photo.setBearing(jpegData.getBearing());
254                 }
255                 // Use file timestamp if exif timestamp isn't available
256                 if (timestamp == null) {
257                         timestamp = new TimestampUtc(inFile.lastModified());
258                 }
259                 // Apply timestamp to photo (but not its point)
260                 photo.setTimestamp(timestamp);
261
262                 return photo;
263         }
264
265
266         /**
267          * Recursively count the selected Files so we can draw a progress bar
268          * @param inFiles file list
269          * @param inFirstDir true if first directory
270          * @param inDescend true to descend to subdirectories
271          * @return count of the files selected
272          */
273         private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
274         {
275                 int fileCount = 0;
276                 if (inFiles != null)
277                 {
278                         // Loop over elements in array
279                         for (int i=0; i<inFiles.length; i++)
280                         {
281                                 File file = inFiles[i];
282                                 if (file.exists() && file.canRead())
283                                 {
284                                         // Store first directory in config for later
285                                         if (i == 0 && inFirstDir) {
286                                                 File workingDir = file.isDirectory()?file:file.getParentFile();
287                                                 Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
288                                         }
289                                         // Check whether it's a file or a directory
290                                         if (file.isFile())
291                                         {
292                                                 fileCount++;
293                                         }
294                                         else if (file.isDirectory() && (inFirstDir || inDescend))
295                                         {
296                                                 fileCount += countFileList(file.listFiles(), false, inDescend);
297                                         }
298                                 }
299                         }
300                 }
301                 return fileCount;
302         }
303
304
305         /**
306          * Create a DataPoint object from the given jpeg data
307          * @param inData Jpeg data including coordinates
308          * @return DataPoint object for Track
309          */
310         private static DataPoint createDataPoint(JpegData inData)
311         {
312                 // Create model objects from jpeg data
313                 double latval = getCoordinateDoubleValue(inData.getLatitude(),
314                         inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
315                 Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
316                 double lonval = getCoordinateDoubleValue(inData.getLongitude(),
317                         inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
318                 Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
319                 Altitude altitude = null;
320                 if (inData.hasAltitude()) {
321                         altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
322                 }
323                 return new DataPoint(latitude, longitude, altitude);
324         }
325
326
327         /**
328          * Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
329          * @param inValues array of three doubles for deg-min-sec
330          * @param isPositive true for positive hemisphere, for positive double value
331          * @return double value of coordinate, either positive or negative
332          */
333         private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
334         {
335                 if (inValues == null || inValues.length != 3) return 0.0;
336                 double value = inValues[0]        // degrees
337                         + inValues[1] / 60.0          // minutes
338                         + inValues[2] / 60.0 / 60.0;  // seconds
339                 // make sure it's the correct sign
340                 value = Math.abs(value);
341                 if (!isPositive) value = -value;
342                 return value;
343         }
344
345
346         /**
347          * Use the given int values to create a timestamp
348          * @param inDate ints describing date
349          * @param inTime ints describing time
350          * @return Timestamp object corresponding to inputs
351          */
352         private static Timestamp createTimestamp(int[] inDate, int[] inTime)
353         {
354                 if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
355                         return null;
356                 }
357                 return new TimestampLocal(inDate[0], inDate[1], inDate[2],
358                         inTime[0], inTime[1], inTime[2]);
359         }
360
361
362         /**
363          * Use the given String value to create a timestamp
364          * @param inStamp timestamp from exif
365          * @return Timestamp object corresponding to input
366          */
367         private static Timestamp createTimestamp(String inStamp)
368         {
369                 Timestamp stamp = null;
370                 try
371                 {
372                         stamp = new TimestampLocal(Integer.parseInt(inStamp.substring(0, 4)),
373                                 Integer.parseInt(inStamp.substring(5, 7)),
374                                 Integer.parseInt(inStamp.substring(8, 10)),
375                                 Integer.parseInt(inStamp.substring(11, 13)),
376                                 Integer.parseInt(inStamp.substring(14, 16)),
377                                 Integer.parseInt(inStamp.substring(17)));
378                 }
379                 catch (NumberFormatException nfe) {}
380                 return stamp;
381         }
382 }