package tim.prune.load;
import java.io.File;
import java.util.TreeSet;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import tim.prune.App;
import tim.prune.I18nManager;
import tim.prune.config.Config;
import tim.prune.data.Altitude;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.LatLonRectangle;
import tim.prune.data.Latitude;
import tim.prune.data.Longitude;
import tim.prune.data.Photo;
import tim.prune.data.Timestamp;
import tim.prune.data.UnitSetLibrary;
import tim.prune.function.Cancellable;
import tim.prune.jpeg.ExifGateway;
import tim.prune.jpeg.JpegData;
/**
* Class to manage the loading of Jpegs and dealing with the GPS data from them
*/
public class JpegLoader implements Runnable, Cancellable
{
private App _app = null;
private JFrame _parentFrame = null;
private JFileChooser _fileChooser = null;
private GenericFileFilter _fileFilter = null;
private JCheckBox _subdirCheckbox = null;
private JCheckBox _noExifCheckbox = null;
private JCheckBox _outsideAreaCheckbox = null;
private MediaLoadProgressDialog _progressDialog = null;
private int[] _fileCounts = null;
private boolean _cancelled = false;
private LatLonRectangle _trackRectangle = null;
private TreeSet<Photo> _photos = null;
/**
* Constructor
* @param inApp Application object to inform of photo load
* @param inParentFrame parent frame to reference for dialogs
*/
public JpegLoader(App inApp, JFrame inParentFrame)
{
_app = inApp;
_parentFrame = inParentFrame;
_fileFilter = new JpegFileFilter();
}
/**
* Open the GUI to select options and start the load
* @param inRectangle track rectangle
*/
public void openDialog(LatLonRectangle inRectangle)
{
// Create file chooser if necessary
if (_fileChooser == null)
{
_fileChooser = new JFileChooser();
_fileChooser.setMultiSelectionEnabled(true);
_fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
_fileChooser.setFileFilter(_fileFilter);
_fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
_subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
_subdirCheckbox.setSelected(true);
_noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
_noExifCheckbox.setSelected(true);
_outsideAreaCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegsoutsidearea"));
_outsideAreaCheckbox.setSelected(true);
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(_subdirCheckbox);
panel.add(_noExifCheckbox);
panel.add(_outsideAreaCheckbox);
_fileChooser.setAccessory(panel);
// start from directory in config if already set by other operations
String configDir = Config.getConfigString(Config.KEY_PHOTO_DIR);
if (configDir == null) {configDir = Config.getConfigString(Config.KEY_TRACK_DIR);}
if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
}
// enable/disable track checkbox
_trackRectangle = inRectangle;
_outsideAreaCheckbox.setEnabled(_trackRectangle != null && !_trackRectangle.isEmpty());
// Show file dialog to choose file / directory(ies)
if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
{
// Bring up dialog before starting
_progressDialog = new MediaLoadProgressDialog(_parentFrame, this);
_progressDialog.show();
// start thread for processing
new Thread(this).start();
}
}
/** Cancel */
public void cancel() {
_cancelled = true;
}
/**
* Run method for performing tasks in separate thread
*/
public void run()
{
// Initialise arrays, errors, summaries
_fileCounts = new int[3]; // files, jpegs, gps
_photos = new TreeSet<Photo>(new MediaSorter());
File[] files = _fileChooser.getSelectedFiles();
// Loop recursively over selected files/directories to count files
int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
// Set up the progress bar for this number of files
_progressDialog.showProgress(0, numFiles);
_cancelled = false;
// Process the files recursively and build lists of photos
processFileList(files, true, _subdirCheckbox.isSelected());
_progressDialog.close();
if (_cancelled) {return;}
if (_fileCounts[0] == 0)
{
// No files found at all
_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nofilesfound");
}
else if (_fileCounts[1] == 0)
{
// No jpegs found
_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nojpegsfound");
}
else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
{
// Need coordinates but no gps information found
_app.showErrorMessage("error.jpegload.dialogtitle", "error.jpegload.nogpsfound");
}
else
{
// Found some photos to load - pass information back to app
_app.informPhotosLoaded(_photos);
}
}
/**
* Process a list of files and/or directories
* @param inFiles array of file/directories
* @param inFirstDir true if first directory
* @param inDescend true to descend to subdirectories
*/
private void processFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
{
if (inFiles == null) return;
// Loop over elements in array
for (int i=0; i<inFiles.length && !_cancelled; i++)
{
File file = inFiles[i];
if (file.exists() && file.canRead())
{
// Check whether it's a file or a directory
if (file.isFile())
{
processFile(file);
}
else if (file.isDirectory() && (inFirstDir || inDescend))
{
// Always process first directory,
// only process subdirectories if checkbox selected
File[] files = file.listFiles();
processFileList(files, false, inDescend);
}
}
// if file doesn't exist or isn't readable - ignore
}
}
/**
* Process the given file, by attempting to extract its tags
* @param inFile file object to read
*/
private void processFile(File inFile)
{
// Update progress bar
_fileCounts[0]++; // file found
_progressDialog.showProgress(_fileCounts[0], -1);
// Check whether filename corresponds with accepted filenames
if (!_fileFilter.acceptFilename(inFile.getName())) {return;}
// If it's a Jpeg, we can use ExifReader to get coords, otherwise we could try exiftool (if it's installed)
if (inFile.exists() && inFile.canRead()) {
_fileCounts[1]++; // jpeg found
}
Photo photo = createPhoto(inFile);
if (photo.getDataPoint() != null) {
_fileCounts[2]++; // photo has coordinates
}
// Check the criteria for adding the photo - check whether the photo has coordinates and if so if they're within the rectangle
if ( (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
&& (photo.getDataPoint() == null || !_outsideAreaCheckbox.isEnabled()
|| _outsideAreaCheckbox.isSelected() || _trackRectangle.containsPoint(photo.getDataPoint())))
{
_photos.add(photo);
}
}
/**
* Create a Photo object for the given file, including reading exif information
* @param inFile file object
* @return Photo object
*/
public static Photo createPhoto(File inFile)
{
// Create Photo object
Photo photo = new Photo(inFile);
// Try to get information out of exif
JpegData jpegData = ExifGateway.getJpegData(inFile);
Timestamp timestamp = null;
if (jpegData != null)
{
if (jpegData.isGpsValid())
{
timestamp = createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp());
// Make DataPoint and attach to Photo
DataPoint point = createDataPoint(jpegData);
point.setPhoto(photo);
point.setSegmentStart(true);
photo.setDataPoint(point);
photo.setOriginalStatus(Photo.Status.TAGGED);
}
// Use exif timestamp if gps timestamp not available
if (timestamp == null && jpegData.getOriginalTimestamp() != null) {
timestamp = createTimestamp(jpegData.getOriginalTimestamp());
}
if (timestamp == null && jpegData.getDigitizedTimestamp() != null) {
timestamp = createTimestamp(jpegData.getDigitizedTimestamp());
}
photo.setExifThumbnail(jpegData.getThumbnailImage());
// Also extract orientation tag for setting rotation state of photo
photo.setRotation(jpegData.getRequiredRotation());
// Set bearing, if any
photo.setBearing(jpegData.getBearing());
}
// Use file timestamp if exif timestamp isn't available
if (timestamp == null) {
timestamp = new Timestamp(inFile.lastModified());
}
// Apply timestamp to photo and its point (if any)
photo.setTimestamp(timestamp);
if (photo.getDataPoint() != null) {
photo.getDataPoint().setFieldValue(Field.TIMESTAMP, timestamp.getText(Timestamp.Format.ISO8601), false);
}
return photo;
}
/**
* Recursively count the selected Files so we can draw a progress bar
* @param inFiles file list
* @param inFirstDir true if first directory
* @param inDescend true to descend to subdirectories
* @return count of the files selected
*/
private int countFileList(File[] inFiles, boolean inFirstDir, boolean inDescend)
{
int fileCount = 0;
if (inFiles != null)
{
// Loop over elements in array
for (int i=0; i<inFiles.length; i++)
{
File file = inFiles[i];
if (file.exists() && file.canRead())
{
// Store first directory in config for later
if (i == 0 && inFirstDir) {
File workingDir = file.isDirectory()?file:file.getParentFile();
Config.setConfigString(Config.KEY_PHOTO_DIR, workingDir.getAbsolutePath());
}
// Check whether it's a file or a directory
if (file.isFile())
{
fileCount++;
}
else if (file.isDirectory() && (inFirstDir || inDescend))
{
fileCount += countFileList(file.listFiles(), false, inDescend);
}
}
}
}
return fileCount;
}
/**
* Create a DataPoint object from the given jpeg data
* @param inData Jpeg data including coordinates
* @return DataPoint object for Track
*/
private static DataPoint createDataPoint(JpegData inData)
{
// Create model objects from jpeg data
double latval = getCoordinateDoubleValue(inData.getLatitude(),
inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC);
double lonval = getCoordinateDoubleValue(inData.getLongitude(),
inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e');
Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
Altitude altitude = null;
if (inData.hasAltitude()) {
altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES);
}
return new DataPoint(latitude, longitude, altitude);
}
/**
* Convert an array of 3 doubles (deg-min-sec) into a double coordinate value
* @param inValues array of three doubles for deg-min-sec
* @param isPositive true for positive hemisphere, for positive double value
* @return double value of coordinate, either positive or negative
*/
private static double getCoordinateDoubleValue(double[] inValues, boolean isPositive)
{
if (inValues == null || inValues.length != 3) return 0.0;
double value = inValues[0] // degrees
+ inValues[1] / 60.0 // minutes
+ inValues[2] / 60.0 / 60.0; // seconds
// make sure it's the correct sign
value = Math.abs(value);
if (!isPositive) value = -value;
return value;
}
/**
* Use the given int values to create a timestamp
* @param inDate ints describing date
* @param inTime ints describing time
* @return Timestamp object corresponding to inputs
*/
private static Timestamp createTimestamp(int[] inDate, int[] inTime)
{
if (inDate == null || inTime == null || inDate.length != 3 || inTime.length != 3) {
return null;
}
return new Timestamp(inDate[0], inDate[1], inDate[2],
inTime[0], inTime[1], inTime[2]);
}
/**
* Use the given String value to create a timestamp
* @param inStamp timestamp from exif
* @return Timestamp object corresponding to input
*/
private static Timestamp createTimestamp(String inStamp)
{
Timestamp stamp = null;
try
{
stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
Integer.parseInt(inStamp.substring(5, 7)),
Integer.parseInt(inStamp.substring(8, 10)),
Integer.parseInt(inStamp.substring(11, 13)),
Integer.parseInt(inStamp.substring(14, 16)),
Integer.parseInt(inStamp.substring(17)));
}
catch (NumberFormatException nfe) {}
return stamp;
}
}