]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Version 3, August 2007
authoractivityworkshop <mail@activityworkshop.net>
Sat, 14 Feb 2015 13:43:11 +0000 (14:43 +0100)
committeractivityworkshop <mail@activityworkshop.net>
Sat, 14 Feb 2015 13:43:11 +0000 (14:43 +0100)
63 files changed:
tim/prune/App.java
tim/prune/ExternalTools.java [new file with mode: 0644]
tim/prune/GpsPruner.java
tim/prune/UpdateMessageBroker.java
tim/prune/data/Altitude.java
tim/prune/data/Coordinate.java
tim/prune/data/DataPoint.java
tim/prune/data/Distance.java
tim/prune/data/Photo.java
tim/prune/data/PhotoList.java
tim/prune/data/PhotoStatus.java [new file with mode: 0644]
tim/prune/data/Selection.java
tim/prune/data/Timestamp.java
tim/prune/data/Track.java
tim/prune/data/TrackInfo.java
tim/prune/edit/PointNameEditor.java
tim/prune/gui/AboutScreen.java
tim/prune/gui/DetailsDisplay.java
tim/prune/gui/ImageUtils.java [new file with mode: 0644]
tim/prune/gui/MapChart.java
tim/prune/gui/MenuManager.java
tim/prune/gui/PhotoThumbnail.java [new file with mode: 0644]
tim/prune/gui/ProfileChart.java
tim/prune/gui/SelectorDisplay.java [new file with mode: 0644]
tim/prune/gui/images/add_photo_icon.png [new file with mode: 0644]
tim/prune/gui/images/add_textfile_icon.png [new file with mode: 0644]
tim/prune/gui/images/connect_photo_icon.png [new file with mode: 0644]
tim/prune/gui/images/edit_point_icon.gif [new file with mode: 0644]
tim/prune/gui/images/save_icon.gif [new file with mode: 0644]
tim/prune/gui/images/set_end_icon.png [new file with mode: 0644]
tim/prune/gui/images/set_start_icon.png [new file with mode: 0644]
tim/prune/gui/images/undo_icon.gif [new file with mode: 0644]
tim/prune/lang/prune-texts.properties
tim/prune/lang/prune-texts_de.properties
tim/prune/lang/prune-texts_de_CH.properties
tim/prune/lang/prune-texts_es.properties
tim/prune/lang/prune-texts_fr.properties [new file with mode: 0644]
tim/prune/load/FileCacher.java
tim/prune/load/FileLoader.java
tim/prune/load/JpegLoader.java
tim/prune/load/PhotoMeasurer.java [new file with mode: 0644]
tim/prune/load/TextFileLoader.java [new file with mode: 0644]
tim/prune/load/xml/GpxHandler.java [new file with mode: 0644]
tim/prune/load/xml/KmlHandler.java [new file with mode: 0644]
tim/prune/load/xml/XmlFileLoader.java [new file with mode: 0644]
tim/prune/load/xml/XmlHandler.java [new file with mode: 0644]
tim/prune/readme.txt
tim/prune/save/ExifSaver.java [new file with mode: 0644]
tim/prune/save/KmlExporter.java
tim/prune/save/PhotoTableEntry.java [new file with mode: 0644]
tim/prune/save/PhotoTableModel.java [new file with mode: 0644]
tim/prune/save/PovExporter.java
tim/prune/threedee/Java3DWindow.java
tim/prune/threedee/LineDialog.java [new file with mode: 0644]
tim/prune/threedee/ThreeDModel.java
tim/prune/threedee/WindowFactory.java
tim/prune/undo/UndoConnectPhoto.java [new file with mode: 0644]
tim/prune/undo/UndoDeletePhoto.java [new file with mode: 0644]
tim/prune/undo/UndoDeletePoint.java
tim/prune/undo/UndoDeleteRange.java
tim/prune/undo/UndoEditPoint.java
tim/prune/undo/UndoLoad.java
tim/prune/undo/UndoLoadPhotos.java

index 4ccaed688e40dcfd1aef3b1d7e464fad4b1b26ee..e403722217d51bbe889168d7e66b03b3496899f8 100644 (file)
@@ -9,6 +9,8 @@ import javax.swing.JOptionPane;
 
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoList;
 import tim.prune.data.Track;
 import tim.prune.data.TrackInfo;
 import tim.prune.edit.FieldEditList;
@@ -18,6 +20,8 @@ import tim.prune.gui.MenuManager;
 import tim.prune.gui.UndoManager;
 import tim.prune.load.FileLoader;
 import tim.prune.load.JpegLoader;
+import tim.prune.load.PhotoMeasurer;
+import tim.prune.save.ExifSaver;
 import tim.prune.save.FileSaver;
 import tim.prune.save.KmlExporter;
 import tim.prune.save.PovExporter;
@@ -25,7 +29,9 @@ import tim.prune.threedee.ThreeDException;
 import tim.prune.threedee.ThreeDWindow;
 import tim.prune.threedee.WindowFactory;
 import tim.prune.undo.UndoCompress;
+import tim.prune.undo.UndoConnectPhoto;
 import tim.prune.undo.UndoDeleteDuplicates;
+import tim.prune.undo.UndoDeletePhoto;
 import tim.prune.undo.UndoDeletePoint;
 import tim.prune.undo.UndoDeleteRange;
 import tim.prune.undo.UndoEditPoint;
@@ -51,6 +57,7 @@ public class App
        private MenuManager _menuManager = null;
        private FileLoader _fileLoader = null;
        private JpegLoader _jpegLoader = null;
+       private KmlExporter _exporter = null;
        private PovExporter _povExporter = null;
        private Stack _undoStack = null;
        private UpdateMessageBroker _broker = null;
@@ -91,7 +98,8 @@ public class App
         */
        public boolean hasDataUnsaved()
        {
-               return _undoStack.size() > _lastSavePosition;
+               return (_undoStack.size() > _lastSavePosition
+                       && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
        }
 
        /**
@@ -164,8 +172,12 @@ public class App
                }
                else
                {
-                       KmlExporter exporter = new KmlExporter(this, _frame, _track);
-                       exporter.showDialog();
+                       // Invoke the export
+                       if (_exporter == null)
+                       {
+                               _exporter = new KmlExporter(_frame, _trackInfo);
+                       }
+                       _exporter.showDialog();
                }
        }
 
@@ -210,7 +222,7 @@ public class App
                        // Make new exporter if necessary
                        if (_povExporter == null)
                        {
-                               _povExporter = new PovExporter(this, _frame, _track);
+                               _povExporter = new PovExporter(_frame, _track);
                        }
                        // Specify angles if necessary
                        if (inDefineSettings)
@@ -229,6 +241,9 @@ public class App
         */
        public void exit()
        {
+               // grab focus
+               _frame.toFront();
+               _frame.requestFocus();
                // check if ok to exit
                Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
                if (!hasDataUnsaved()
@@ -292,7 +307,7 @@ public class App
                        {
                                // Open point dialog to display details
                                PointNameEditor editor = new PointNameEditor(this, _frame);
-                               editor.showDialog(_track, currentPoint);
+                               editor.showDialog(currentPoint);
                        }
                }
        }
@@ -308,13 +323,44 @@ public class App
                        DataPoint currentPoint = _trackInfo.getCurrentPoint();
                        if (currentPoint != null)
                        {
+                               boolean deletePhoto = false;
+                               Photo currentPhoto = currentPoint.getPhoto();
+                               if (currentPhoto != null)
+                               {
+                                       // Confirm deletion of photo or decoupling
+                                       int response = JOptionPane.showConfirmDialog(_frame,
+                                               I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
+                                               I18nManager.getText("dialog.deletepoint.title"),
+                                               JOptionPane.YES_NO_CANCEL_OPTION);
+                                       if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
+                                       {
+                                               // cancel pressed- abort delete
+                                               return;
+                                       }
+                                       if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
+                               }
                                // add information to undo stack
                                int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
-                               UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint);
+                               int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
+                               // Undo object needs to know index of photo in list (if any) to restore
+                               UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex);
                                // call track to delete point
                                if (_trackInfo.deletePoint())
                                {
                                        _undoStack.push(undo);
+                                       if (currentPhoto != null)
+                                       {
+                                               // delete photo if necessary
+                                               if (deletePhoto)
+                                               {
+                                                       _trackInfo.getPhotoList().deletePhoto(photoIndex);
+                                               }
+                                               else
+                                               {
+                                                       // decouple photo from point
+                                                       currentPhoto.setDataPoint(null);
+                                               }
+                                       }
                                }
                        }
                }
@@ -328,12 +374,77 @@ public class App
        {
                if (_track != null)
                {
-                       // add information to undo stack
-                       UndoOperation undo = new UndoDeleteRange(_trackInfo);
-                       // call track to delete point
-                       if (_trackInfo.deleteRange())
+                       // Find out if photos should be deleted or not
+                       int selStart = _trackInfo.getSelection().getStart();
+                       int selEnd = _trackInfo.getSelection().getEnd();
+                       if (selStart >= 0 && selEnd >= selStart)
                        {
-                               _undoStack.push(undo);
+                               int numToDelete = selEnd - selStart + 1;
+                               boolean[] deletePhotos = new boolean[numToDelete];
+                               Photo[] photosToDelete = new Photo[numToDelete];
+                               boolean deleteAll = false;
+                               boolean deleteNone = false;
+                               String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
+                                       I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
+                                       I18nManager.getText("button.cancel")};
+                               DataPoint point = null;
+                               for (int i=0; i<numToDelete; i++)
+                               {
+                                       point = _trackInfo.getTrack().getPoint(i + selStart);
+                                       if (point != null && point.getPhoto() != null)
+                                       {
+                                               if (deleteAll)
+                                               {
+                                                       deletePhotos[i] = true;
+                                                       photosToDelete[i] = point.getPhoto();
+                                               }
+                                               else if (deleteNone) {deletePhotos[i] = false;}
+                                               else
+                                               {
+                                                       int response = JOptionPane.showOptionDialog(_frame,
+                                                               I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
+                                                               I18nManager.getText("dialog.deletepoint.title"),
+                                                               JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
+                                                               questionOptions, questionOptions[1]);
+                                                       // check for cancel or close
+                                                       if (response == 4 || response == -1) {return;}
+                                                       // check for yes or yes to all
+                                                       if (response == 0 || response == 2)
+                                                       {
+                                                               deletePhotos[i] = true;
+                                                               photosToDelete[i] = point.getPhoto();
+                                                               if (response == 2) {deleteAll = true;}
+                                                       }
+                                                       // check for no to all
+                                                       if (response == 3) {deleteNone = true;}
+                                               }
+                                       }
+                               }
+                               // add information to undo stack
+                               UndoOperation undo = new UndoDeleteRange(_trackInfo);
+                               // delete requested photos
+                               for (int i=0; i<numToDelete; i++)
+                               {
+                                       point = _trackInfo.getTrack().getPoint(i + selStart);
+                                       if (point != null && point.getPhoto() != null)
+                                       {
+                                               if (deletePhotos[i])
+                                               {
+                                                       // delete photo from list
+                                                       _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
+                                               }
+                                               else
+                                               {
+                                                       // decouple from point
+                                                       point.getPhoto().setDataPoint(null);
+                                               }
+                                       }
+                               }
+                               // call track to delete range
+                               if (_trackInfo.deleteRange())
+                               {
+                                       _undoStack.push(undo);
+                               }
                        }
                }
        }
@@ -527,9 +638,11 @@ public class App
         */
        public void selectNone()
        {
+               // deselect point, range and photo
                _trackInfo.getSelection().clearAll();
        }
 
+
        /**
         * Receive loaded data and optionally merge with current Track
         * @param inFieldArray array of fields
@@ -537,6 +650,17 @@ public class App
         */
        public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
        {
+               // Check whether loaded array can be properly parsed into a Track
+               Track loadedTrack = new Track(_broker);
+               loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
+               if (loadedTrack.getNumPoints() <= 0)
+               {
+                       JOptionPane.showMessageDialog(_frame,
+                               I18nManager.getText("error.load.nopoints"),
+                               I18nManager.getText("error.load.dialogtitle"),
+                               JOptionPane.ERROR_MESSAGE);
+                       return;
+               }
                // Decide whether to load or append
                if (_track != null && _track.getNumPoints() > 0)
                {
@@ -548,25 +672,41 @@ public class App
                        if (answer == JOptionPane.YES_OPTION)
                        {
                                // append data to current Track
-                               Track loadedTrack = new Track(_broker);
-                               loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
                                _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
                                _track.combine(loadedTrack);
-                               _trackInfo.getFileInfo().addFile();
+                               // set filename if currently empty
+                               if (_trackInfo.getFileInfo().getNumFiles() == 0)
+                               {
+                                       _trackInfo.getFileInfo().setFile(inFilename);
+                               }
+                               else
+                               {
+                                       _trackInfo.getFileInfo().addFile();
+                               }
                        }
                        else if (answer == JOptionPane.NO_OPTION)
                        {
                                // Don't append, replace data
-                               _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length));
+                               PhotoList photos = null;
+                               if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
+                               {
+                                       photos = _trackInfo.getPhotoList().cloneList();
+                               }
+                               _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
                                _lastSavePosition = _undoStack.size();
+                               // TODO: Should be possible to reuse the Track object already loaded?
                                _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
                                _trackInfo.getFileInfo().setFile(inFilename);
+                               if (photos != null)
+                               {
+                                       _trackInfo.getPhotoList().removeCorrelatedPhotos();
+                               }
                        }
                }
                else
                {
                        // currently no data held, so use received data
-                       _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length));
+                       _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
                        _lastSavePosition = _undoStack.size();
                        _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
                        _trackInfo.getFileInfo().setFile(inFilename);
@@ -585,22 +725,26 @@ public class App
        {
                if (inPhotoList != null && !inPhotoList.isEmpty())
                {
-                       // TODO: Attempt to restrict loaded photos to current area (if any) ?
-                       int numAdded = _trackInfo.addPhotos(inPhotoList);
-                       if (numAdded > 0)
+                       int[] numsAdded = _trackInfo.addPhotos(inPhotoList);
+                       int numPhotosAdded = numsAdded[0];
+                       int numPointsAdded = numsAdded[1];
+                       if (numPhotosAdded > 0)
                        {
-                               _undoStack.add(new UndoLoadPhotos(numAdded));
+                               // Save numbers so load can be undone
+                               _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
+                               // Trigger preloading of photo sizes in separate thread
+                               new PhotoMeasurer(_trackInfo.getPhotoList()).measurePhotos();
                        }
-                       if (numAdded == 1)
+                       if (numPhotosAdded == 1)
                        {
                                JOptionPane.showMessageDialog(_frame,
-                                       "" + numAdded + " " + I18nManager.getText("dialog.jpegload.photoadded"),
+                                       "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photoadded"),
                                        I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE);
                        }
                        else
                        {
                                JOptionPane.showMessageDialog(_frame,
-                                       "" + numAdded + " " + I18nManager.getText("dialog.jpegload.photosadded"),
+                                       "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photosadded"),
                                        I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE);
                        }
                        // TODO: Improve message when photo(s) fail to load (eg already added)
@@ -611,6 +755,78 @@ public class App
        }
 
 
+       /**
+        * Connect the current photo to the current point
+        */
+       public void connectPhotoToPoint()
+       {
+               Photo photo = _trackInfo.getCurrentPhoto();
+               DataPoint point = _trackInfo.getCurrentPoint();
+               if (photo != null && point != null && point.getPhoto() == null)
+               {
+                       // connect
+                       _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
+                       photo.setDataPoint(point);
+                       point.setPhoto(photo);
+                       //TODO: Confirm connect (maybe with status in photo panel?)
+               }
+       }
+
+
+       /**
+        * Remove the current photo, if any
+        */
+       public void deleteCurrentPhoto()
+       {
+               // Delete the current photo, and optionally its point too, keeping undo information
+               Photo currentPhoto = _trackInfo.getCurrentPhoto();
+               if (currentPhoto != null)
+               {
+                       // Photo is selected, see if it has a point or not
+                       boolean photoDeleted = false;
+                       UndoDeletePhoto undoAction = null;
+                       if (currentPhoto.getDataPoint() == null)
+                       {
+                               // no point attached, so just delete photo
+                               undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
+                                       null, -1);
+                               photoDeleted = _trackInfo.deleteCurrentPhoto(false);
+                       }
+                       else
+                       {
+                               // point is attached, so need to confirm point deletion
+                               undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
+                                       currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
+                               int response = JOptionPane.showConfirmDialog(_frame,
+                                       I18nManager.getText("dialog.deletephoto.deletepoint"),
+                                       I18nManager.getText("dialog.deletephoto.title"),
+                                       JOptionPane.YES_NO_CANCEL_OPTION);
+                               boolean deletePointToo = (response == JOptionPane.YES_OPTION);
+                               // Cancel delete if cancel pressed or dialog closed
+                               if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
+                               {
+                                       photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
+                               }
+                       }
+                       // Add undo information to stack if necessary
+                       if (photoDeleted)
+                       {
+                               _undoStack.add(undoAction);
+                       }
+               }
+       }
+
+
+       /**
+        * Save the coordinates of photos in their exif data
+        */
+       public void saveExif()
+       {
+               ExifSaver saver = new ExifSaver(_frame);
+               saver.saveExifInformation(_trackInfo.getPhotoList());
+       }
+
+
        /**
         * Inform the app that the data has been saved
         */
diff --git a/tim/prune/ExternalTools.java b/tim/prune/ExternalTools.java
new file mode 100644 (file)
index 0000000..cd65fff
--- /dev/null
@@ -0,0 +1,48 @@
+package tim.prune;
+
+import java.io.IOException;
+
+
+/**
+ * Class to manage interfaces to external tools, like exiftool
+ */
+public abstract class ExternalTools
+{
+
+       /**
+        * Attempt to call Povray to see if it's installed / available in path
+        * @return true if found, false otherwise
+        */
+       public static boolean isPovrayInstalled()
+       {
+               try
+               {
+                       Runtime.getRuntime().exec("povray");
+                       return true;
+               }
+               catch (IOException ioe)
+               {
+                       // exception thrown, povray not found
+                       return false;
+               }
+       }
+
+
+       /**
+        * Attempt to call Exiftool to see if it's installed / available in path
+        * @return true if found, false otherwise
+        */
+       public static boolean isExiftoolInstalled()
+       {
+               try
+               {
+                       Runtime.getRuntime().exec("exiftool -v");
+                       return true;
+               }
+               catch (IOException ioe)
+               {
+                       // exception thrown, exiftool not found
+                       return false;
+               }
+       }
+}
index 7e9476cf5b2b62c56604d83a91eb36a821765371..2511cf7981dcba09eafe474290a314edfff8a6f7 100644 (file)
@@ -1,26 +1,29 @@
 package tim.prune;
 
+import java.awt.BorderLayout;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.util.Locale;
 
 import javax.swing.JFrame;
 import javax.swing.JSplitPane;
+import javax.swing.JToolBar;
 import javax.swing.WindowConstants;
 
 import tim.prune.gui.DetailsDisplay;
 import tim.prune.gui.MapChart;
 import tim.prune.gui.MenuManager;
 import tim.prune.gui.ProfileChart;
+import tim.prune.gui.SelectorDisplay;
 
 /**
  * Tool to visualize, edit and prune GPS data
  */
 public class GpsPruner
 {
-       // Version 2, released 29 March 2007, 1 April 2007
-       public static final String VERSION_NUMBER = "2";
-       public static final String BUILD_NUMBER = "056";
+       // Final release of version 3
+       public static final String VERSION_NUMBER = "3";
+       public static final String BUILD_NUMBER = "074";
        private static App APP = null;
 
 
@@ -35,23 +38,44 @@ public class GpsPruner
                {
                        if (args[0].startsWith("--locale="))
                        {
-                               if (args[0].length() == 11)
-                                       locale = new Locale(args[0].substring(9));
-                               else if (args[0].length() == 14)
-                                       locale = new Locale(args[0].substring(9, 11), args[0].substring(12));
-                               else
-                                       System.out.println("Unrecognised locale '" + args[0].substring(9)
-                                               + "' - locale should be eg 'DE' or 'DE_ch'");
+                               locale = getLanguage(args[0].substring(9));
+                       }
+                       else if (args[0].startsWith("--lang="))
+                       {
+                               locale = getLanguage(args[0].substring(7));
                        }
                        else
+                       {
                                System.out.println("Unknown parameter '" + args[0] +
-                                       "'. Possible parameters:\n   --locale=  used for overriding locale settings\n");
+                                       "'. Possible parameters:\n   --locale= or --lang=  used for overriding language settings\n");
+                       }
                }
                I18nManager.init(locale);
                launch();
        }
 
 
+       /**
+        * Choose a locale based on the given code
+        * @param inString code for locale
+        * @return Locale object if available, otherwise null
+        */
+       private static Locale getLanguage(String inString)
+       {
+               if (inString.length() == 2)
+               {
+                       return new Locale(inString);
+               }
+               else if (inString.length() == 5)
+               {
+                       return new Locale(inString.substring(0, 2), inString.substring(3));
+               }
+               System.out.println("Unrecognised locale '" + inString
+                       + "' - value should be eg 'DE' or 'DE_ch'");
+               return null;
+       }
+
+
        /**
         * Launch the main application
         */
@@ -66,19 +90,28 @@ public class GpsPruner
                frame.setJMenuBar(menuManager.createMenuBar());
                APP.setMenuManager(menuManager);
                broker.addSubscriber(menuManager);
+               // Make toolbar for buttons
+               JToolBar toolbar = menuManager.createToolBar();
 
                // Make three GUI components and add as listeners
-               DetailsDisplay leftPanel = new DetailsDisplay(APP, APP.getTrackInfo());
+               SelectorDisplay leftPanel = new SelectorDisplay(APP.getTrackInfo());
                broker.addSubscriber(leftPanel);
+               DetailsDisplay rightPanel = new DetailsDisplay(APP.getTrackInfo());
+               broker.addSubscriber(rightPanel);
                MapChart mapDisp = new MapChart(APP, APP.getTrackInfo());
                broker.addSubscriber(mapDisp);
                ProfileChart profileDisp = new ProfileChart(APP.getTrackInfo());
                broker.addSubscriber(profileDisp);
 
-               JSplitPane rightPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mapDisp, profileDisp);
-               rightPane.setResizeWeight(1.0); // allocate as much space as poss to map
+               JSplitPane midPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mapDisp, profileDisp);
+               midPane.setResizeWeight(1.0); // allocate as much space as poss to map
+               JSplitPane triplePane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, midPane, rightPanel);
+               triplePane.setResizeWeight(1.0); // allocate as much space as poss to map
+
+               frame.getContentPane().setLayout(new BorderLayout());
+               frame.getContentPane().add(toolbar, BorderLayout.NORTH);
                frame.getContentPane().add(new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel,
-                               rightPane));
+                       triplePane), BorderLayout.CENTER);
                // add closing listener
                frame.addWindowListener(new WindowAdapter() {
                        public void windowClosing(WindowEvent e) {
@@ -90,7 +123,7 @@ public class GpsPruner
 
                // finish off and display frame
                frame.pack();
-               frame.setSize(600, 450);
+               frame.setSize(650, 450);
                frame.show();
        }
 }
index 417d05547aa9b54e8bf3f7b09c0d11d2edae0f8d..cac2a79dddc08e5a5d68ccc6f3dcca934c73a930 100644 (file)
@@ -8,7 +8,7 @@ public class UpdateMessageBroker
 {
        private DataSubscriber[] _subscribers;
        private int _subscriberNum = 0;
-       private static final int MAXIMUM_NUMBER_SUBSCRIBERS = 4;
+       private static final int MAXIMUM_NUMBER_SUBSCRIBERS = 5;
 
 
        /**
index b55a429200c723e9f511a7a4b769e65befdcc622..54a3f68eb862416791840dcfcb84009a5d191499 100644 (file)
@@ -25,7 +25,7 @@ public class Altitude
                {
                        try
                        {
-                               _value = Integer.parseInt(inString.trim());
+                               _value = (int) Double.parseDouble(inString.trim());
                                _format = inFormat;
                                _valid = true;
                        }
index 6d8de61197bec7ae945658662c1060c4dd0e7663..8a81850cba90e7c429696a3062dc3e5f96681a66 100644 (file)
@@ -15,6 +15,9 @@ public abstract class Coordinate
        public static final int FORMAT_DEG_MIN = 11;
        public static final int FORMAT_DEG = 12;
        public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13;
+       public static final int FORMAT_DEG_WHOLE_MIN = 14;
+       public static final int FORMAT_DEG_MIN_SEC_WITH_SPACES = 15;
+       public static final int FORMAT_CARDINAL = 16;
        public static final int FORMAT_NONE = 19;
 
        // Instance variables
@@ -134,8 +137,8 @@ public abstract class Coordinate
        {
                _asDouble = inValue;
                // Calculate degrees, minutes, seconds
-               _degrees = (int) inValue;
-               double numMins = (Math.abs(_asDouble)-Math.abs(_degrees)) * 60.0;
+               _degrees = (int) Math.abs(inValue);
+               double numMins = (Math.abs(_asDouble)-_degrees) * 60.0;
                _minutes = (int) numMins;
                double numSecs = (numMins - _minutes) * 60.0;
                _seconds = (int) numSecs;
@@ -204,18 +207,37 @@ public abstract class Coordinate
                                                .append(twoDigitString(_minutes)).append('\'')
                                                .append(twoDigitString(_seconds)).append('.')
                                                .append(_fracs);
-                                       answer = buffer.toString(); break;
+                                       answer = buffer.toString();
+                                       break;
                                }
                                case FORMAT_DEG_MIN:
                                {
                                        answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
-                                               + (_minutes + _seconds / 60.0 + _fracs / 600.0); break;
+                                               + (_minutes + _seconds / 60.0 + _fracs / 600.0) + "'";
+                                       break;
+                               }
+                               case FORMAT_DEG_WHOLE_MIN:
+                               {
+                                       answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
+                                               + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 600.0 + 0.5) + "'";
+                                       break;
                                }
                                case FORMAT_DEG:
                                case FORMAT_DEG_WITHOUT_CARDINAL:
                                {
                                        answer = (_asDouble<0.0?"-":"")
-                                               + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 36000.0); break;
+                                               + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 36000.0);
+                                       break;
+                               }
+                               case FORMAT_DEG_MIN_SEC_WITH_SPACES:
+                               {
+                                       answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + _fracs;
+                                       break;
+                               }
+                               case FORMAT_CARDINAL:
+                               {
+                                       answer = "" + PRINTABLE_CARDINALS[_cardinal];
+                                       break;
                                }
                        }
                }
index cd285d66790a83f263b78a44e0ef11ce1b7517cc..46c4acd9d1ad562883ed9d8a8830802d1f123c19 100644 (file)
@@ -17,7 +17,7 @@ public class DataPoint
        private Timestamp _timestamp = null;
        private Photo _photo = null;
        private String _waypointName = null;
-       private boolean _pointValid = false;
+       // private boolean _startOfSegment = false;
 
 
        /**
@@ -48,6 +48,7 @@ public class DataPoint
                _altitude = new Altitude(getFieldValue(Field.ALTITUDE), inAltFormat);
                _timestamp = new Timestamp(getFieldValue(Field.TIMESTAMP));
                _waypointName = getFieldValue(Field.WAYPT_NAME);
+               // TODO: Parse segment start field (format?)
        }
 
 
@@ -60,11 +61,15 @@ public class DataPoint
        public DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude)
        {
                // Only these three fields are available
-               _fieldValues = new String[0];
-               _fieldList = new FieldList();
+               _fieldValues = new String[3];
+               Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE};
+               _fieldList = new FieldList(fields);
                _latitude = inLatitude;
+               _fieldValues[0] = inLatitude.output(Coordinate.FORMAT_DEG_MIN_SEC);
                _longitude = inLongitude;
+               _fieldValues[1] = inLongitude.output(Coordinate.FORMAT_DEG_MIN_SEC);
                _altitude = inAltitude;
+               if (inAltitude != null) {_fieldValues[2] = "" + inAltitude.getValue(Altitude.FORMAT_METRES);}
                _timestamp = new Timestamp(null);
        }
 
index 77ab6d46bc13319aa6829418fd60d5c6c251a3d2..5425873102aa752833c9953c91e967705d0ad75b 100644 (file)
@@ -13,8 +13,8 @@ public abstract class Distance
        private static final double EARTH_RADIUS_KM = 6372.795;
        private static final double EARTH_RADIUS_MILES = 3959.8712255;
        // Conversion constants
-       private static final double CONVERT_KM_TO_MILES = 1.609344;
-       private static final double CONVERT_MILES_TO_KM = 0.621371192;
+       //private static final double CONVERT_KM_TO_MILES = 1.609344;
+       //private static final double CONVERT_MILES_TO_KM = 0.621371192;
 
 
        /**
index 52743f7a34659d40bd6aebb46b85759b0b524902..3646cb5201ba706d3d2aae30fa021d8407a41adf 100644 (file)
@@ -1,7 +1,10 @@
 package tim.prune.data;
 
+import java.awt.Dimension;
 import java.io.File;
 
+import javax.swing.ImageIcon;
+
 /**
  * Class to represent a photo and link to DataPoint
  */
@@ -9,8 +12,18 @@ public class Photo
 {
        /** File where photo is stored */
        private File _file = null;
+       /** Timestamp, if any */
+       private Timestamp _timestamp = null;
        /** Associated DataPoint if correlated */
        private DataPoint _dataPoint = null;
+       /** Size of original image */
+       private Dimension _size = null;
+       /** Status of photo when loaded */
+       private byte _originalStatus = PhotoStatus.NOT_CONNECTED;
+       /** Current photo status */
+       private byte _currentStatus = PhotoStatus.NOT_CONNECTED;
+       // TODO: Need to store caption for image?
+       // TODO: Need to store thumbnail for image?
 
 
        /**
@@ -20,7 +33,6 @@ public class Photo
        public Photo(File inFile)
        {
                _file = inFile;
-               // TODO: Cache photo file contents to allow thumbnail preview
        }
 
 
@@ -40,6 +52,15 @@ public class Photo
        public void setDataPoint(DataPoint inPoint)
        {
                _dataPoint = inPoint;
+               // set status according to point
+               if (inPoint == null)
+               {
+                       setCurrentStatus(PhotoStatus.NOT_CONNECTED);
+               }
+               else
+               {
+                       setCurrentStatus(PhotoStatus.CONNECTED);
+               }
        }
 
        /**
@@ -50,6 +71,116 @@ public class Photo
                return _dataPoint;
        }
 
+       /**
+        * @param inTimestamp Timestamp of photo
+        */
+       public void setTimestamp(Timestamp inTimestamp)
+       {
+               _timestamp = inTimestamp;
+       }
+
+       /**
+        * @return timestamp of photo
+        */
+       public Timestamp getTimestamp()
+       {
+               return _timestamp;
+       }
+
+       /**
+        * Calculate the size of the image (slow)
+        */
+       private void calculateSize()
+       {
+               ImageIcon icon = new ImageIcon(_file.getAbsolutePath());
+               int width = icon.getIconWidth();
+               int height = icon.getIconHeight();
+               if (width > 0 && height > 0)
+               {
+                       _size = new Dimension(width, height);
+               }
+       }
+
+       /**
+        * @return size of image as Dimension object
+        */
+       public Dimension getSize()
+       {
+               if (_size == null)
+               {
+                       calculateSize();
+               }
+               return _size;
+       }
+
+       /**
+        * @return width of the image, if known
+        */
+       public int getWidth()
+       {
+               if (_size == null)
+               {
+                       calculateSize();
+                       if (_size == null) {return -1;}
+               }
+               return _size.width;
+       }
+
+       /**
+        * @return height of the image, if known
+        */
+       public int getHeight()
+       {
+               if (_size == null)
+               {
+                       calculateSize();
+                       if (_size == null) {return -1;}
+               }
+               return _size.height;
+       }
+
+       /**
+        * @param inStatus status of photo when loaded
+        */
+       public void setOriginalStatus(byte inStatus)
+       {
+               _originalStatus = inStatus;
+               _currentStatus = inStatus;
+       }
+
+       /**
+        * @return status of photo when it was loaded
+        */
+       public byte getOriginalStatus()
+       {
+               return _originalStatus;
+       }
+
+       /**
+        * @return current status of photo
+        */
+       public byte getCurrentStatus()
+       {
+               return _currentStatus;
+       }
+       /**
+        * @param inStatus current status of photo
+        */
+       public void setCurrentStatus(byte inStatus)
+       {
+               _currentStatus = inStatus;
+       }
+
+
+       /**
+        * Delete the cached data when the Photo is no longer needed
+        */
+       public void resetCachedData()
+       {
+               _size = null;
+               // remove thumbnail too
+       }
+
        /**
         * Check if a Photo object refers to the same File as another
         * @param inOther other Photo object
index ac10edfafd8d3335dd87f50fb48c4b0e46f0637a..b7c044da5ad2295b2bd98d677fc1c1a84d1aca2d 100644 (file)
@@ -9,6 +9,24 @@ public class PhotoList
 {
        private ArrayList _photos = null;
 
+       /**
+        * Empty constructor
+        */
+       public PhotoList()
+       {
+               this(null);
+       }
+
+       /**
+        * Constructor
+        * @param inList ArrayList containing Photo objects
+        */
+       private PhotoList(ArrayList inList)
+       {
+               _photos = inList;
+       }
+
+
        /**
         * @return the number of photos in the list
         */
@@ -20,20 +38,54 @@ public class PhotoList
 
 
        /**
-        * Add a List of Photos
-        * @param inList List containing Photo objects
+        * Add a Photo to the list
+        * @param inPhoto Photo object to add
         */
        public void addPhoto(Photo inPhoto)
        {
-               // Make sure array is initialised
-               if (_photos == null)
+               if (inPhoto != null)
                {
-                       _photos = new ArrayList();
+                       // Make sure array is initialised
+                       if (_photos == null)
+                       {
+                               _photos = new ArrayList();
+                       }
+                       // Add the photo
+                       _photos.add(inPhoto);
                }
-               // Add the photo
+       }
+
+
+       /**
+        * Add a Photo to the list
+        * @param inPhoto Photo object to add
+        * @param inIndex index at which to add photo
+        */
+       public void addPhoto(Photo inPhoto, int inIndex)
+       {
                if (inPhoto != null)
                {
-                       _photos.add(inPhoto);
+                       // Make sure array is initialised
+                       if (_photos == null)
+                       {
+                               _photos = new ArrayList();
+                       }
+                       // Add the photo
+                       _photos.add(inIndex, inPhoto);
+               }
+       }
+
+
+       /**
+        * Remove the selected photo from the list
+        * @param inIndex index number to remove
+        */
+       public void deletePhoto(int inIndex)
+       {
+               // Maybe throw exception if this fails?
+               if (_photos != null)
+               {
+                       _photos.remove(inIndex);
                }
        }
 
@@ -44,20 +96,34 @@ public class PhotoList
         * @return true if it's already in the list
         */
        public boolean contains(Photo inPhoto)
+       {
+               return (getPhotoIndex(inPhoto) > -1);
+       }
+
+
+       /**
+        * Get the index of the given Photo
+        * @param inPhoto Photo object to check
+        * @return index of this Photo in the list, or -1 if not found
+        */
+       public int getPhotoIndex(Photo inPhoto)
        {
                // Check if we need to check
-               if (getNumPhotos() <= 0 || inPhoto == null || inPhoto.getFile() == null)
-                       return false;
+               int numPhotos = getNumPhotos();
+               if (numPhotos <= 0 || inPhoto == null || inPhoto.getFile() == null)
+                       return -1;
                // Loop around photos in list
-               for (int i=0; i<getNumPhotos(); i++)
+               Photo foundPhoto = null;
+               for (int i=0; i<numPhotos; i++)
                {
-                       if (getPhoto(i) != null && getPhoto(i).equals(inPhoto))
+                       foundPhoto = getPhoto(i);
+                       if (foundPhoto != null && foundPhoto.equals(inPhoto))
                        {
-                               return true;
+                               return i;
                        }
                }
                // not found
-               return false;
+               return -1;
        }
 
 
@@ -82,7 +148,7 @@ public class PhotoList
                if (inIndex <= 0)
                {
                        // delete whole list
-                       _photos.clear();
+                       if (_photos != null) {_photos.clear();}
                }
                else
                {
@@ -94,6 +160,7 @@ public class PhotoList
                }
        }
 
+
        /**
         * @return array of file names
         */
@@ -106,4 +173,80 @@ public class PhotoList
                }
                return names;
        }
+
+
+       /**
+        * @return true if photo list contains correlated photos
+        */
+       public boolean hasCorrelatedPhotos()
+       {
+               int numPhotos = getNumPhotos();
+               boolean hasCorrelated = false;
+               // Loop over photos in list
+               for (int i=0; i<numPhotos && !hasCorrelated; i++)
+               {
+                       if (getPhoto(i).getDataPoint() != null)
+                               hasCorrelated = true;
+               }
+               return hasCorrelated;
+       }
+
+
+       /**
+        * Remove all correlated photos from the list
+        */
+       public void removeCorrelatedPhotos()
+       {
+               int numPhotos = getNumPhotos();
+               if (numPhotos > 0)
+               {
+                       // Construct new list to copy into
+                       ArrayList listCopy = new ArrayList();
+                       // Loop over photos in list
+                       for (int i=0; i<numPhotos; i++)
+                       {
+                               // Copy photo if it has no point
+                               Photo photo = getPhoto(i);
+                               if (photo != null)
+                               {
+                                       if (photo.getDataPoint() == null)
+                                               listCopy.add(photo);
+                                       else
+                                               photo.resetCachedData();
+                               }
+                       }
+                       // Switch reference to new list
+                       _photos = listCopy;
+               }
+       }
+
+
+       /**
+        * @return clone of photo list contents
+        */
+       public PhotoList cloneList()
+       {
+               if (_photos == null) return this;
+               return new PhotoList((ArrayList) _photos.clone());
+       }
+
+
+       /**
+        * Restore contents from other PhotoList
+        * @param inOther PhotoList with cloned contents
+        */
+       public void restore(PhotoList inOther)
+       {
+               if (inOther.getNumPhotos() == 0)
+               {
+                       // List is empty
+                       _photos = null;
+               }
+               else
+               {
+                       // Clear array and copy over from other one
+                       _photos.clear();
+                       _photos.addAll(inOther._photos);
+               }
+       }
 }
diff --git a/tim/prune/data/PhotoStatus.java b/tim/prune/data/PhotoStatus.java
new file mode 100644 (file)
index 0000000..831f36f
--- /dev/null
@@ -0,0 +1,11 @@
+package tim.prune.data;
+
+/**
+ * Interface to hold constants for photo status
+ */
+public interface PhotoStatus
+{
+       public static final byte NOT_CONNECTED = 0;
+       public static final byte TAGGED        = 1;
+       public static final byte CONNECTED     = 2;
+}
index f09088dc218e6f498cef7d78a498a5995e019c02..4849a4a42bcddb948172bf494f9180d5639eb9a0 100644 (file)
@@ -13,6 +13,7 @@ public class Selection
        private int _currentPoint = -1;
        private boolean _valid = false;
        private int _startIndex = -1, _endIndex = -1;
+       private int _currentPhotoIndex = -1;
        private IntegerRange _altitudeRange = null;
        private int _climb = -1, _descent = -1;
        private int _altitudeFormat = Altitude.FORMAT_NONE;
@@ -258,6 +259,7 @@ public class Selection
        {
                _currentPoint = -1;
                deselectRange();
+               deselectPhoto();
        }
 
 
@@ -377,6 +379,47 @@ public class Selection
        }
 
 
+       /**
+        * Deselect photo
+        */
+       public void deselectPhoto()
+       {
+               _currentPhotoIndex = -1;
+               check();
+       }
+
+
+       /**
+        * Select the specified photo and point
+        * @param inPhotoIndex index of selected photo in PhotoList
+        * @param inPointIndex index of selected point
+        */
+       public void selectPhotoAndPoint(int inPhotoIndex, int inPointIndex)
+       {
+               _currentPhotoIndex = inPhotoIndex;
+               if (inPointIndex > -1)
+               {
+                       // select associated point, if any
+                       selectPoint(inPointIndex);
+               }
+               else
+               {
+                       // Check if not already done
+                       check();
+               }
+       }
+
+
+       /**
+        * @return currently selected photo index
+        */
+       public int getCurrentPhotoIndex()
+       {
+               // System.out.println("Current photo index = " + _currentPhotoIndex);
+               return _currentPhotoIndex;
+       }
+
+
        /**
         * Check that the selection still makes sense
         * and fire update message to listeners
index 965853ad036e5b61a44fe1dc58af87154bfc7df0..ddb380818fc45d091c92ccd57111b5ae40d34eee 100644 (file)
@@ -53,6 +53,7 @@ public class Timestamp
         */
        public Timestamp(String inString)
        {
+               // TODO: Does it really help to store timestamps in seconds rather than ms?
                if (inString != null && !inString.equals(""))
                {
                        // Try to parse into a long
@@ -85,7 +86,7 @@ public class Timestamp
                                // Lastly, check garmin offset
                                if (diff4 < smallestDiff)
                                {
-                                       // milliseconds since garmin offset
+                                       // seconds since garmin offset
                                        _seconds = rawValue + GARTRIP_OFFSET;
                                }
                                _valid = true;
@@ -111,6 +112,41 @@ public class Timestamp
        }
 
 
+       /**
+        * Constructor giving each field value individually
+        * @param inYear year
+        * @param inMonth month, beginning with 1
+        * @param inDay day of month, beginning with 1
+        * @param inHour hour of day, 0-24
+        * @param inMinute minute
+        * @param inSecond seconds
+        */
+       public Timestamp(int inYear, int inMonth, int inDay, int inHour, int inMinute, int inSecond)
+       {
+               Calendar cal = Calendar.getInstance();
+               cal.set(Calendar.YEAR, inYear);
+               cal.set(Calendar.MONTH, inMonth - 1);
+               cal.set(Calendar.DAY_OF_MONTH, inDay);
+               cal.set(Calendar.HOUR_OF_DAY, inHour);
+               cal.set(Calendar.MINUTE, inMinute);
+               cal.set(Calendar.SECOND, inSecond);
+               cal.set(Calendar.MILLISECOND, 0);
+               _seconds = cal.getTimeInMillis() / 1000;
+               _valid = true;
+       }
+
+
+       /**
+        * Constructor giving millis since 1970
+        * @param inMillis
+        */
+       public Timestamp(long inMillis)
+       {
+               _seconds = inMillis / 1000;
+               _valid = true;
+       }
+
+
        /**
         * @return true if timestamp is valid
         */
index e73fa736eb6731d1d7c86ee5d48dffc6bf35b0eb..c5a0f16f4d72a3707ebf4460088958c97cbf14c0 100644 (file)
@@ -57,6 +57,11 @@ public class Track
         */
        public void load(Field[] inFieldArray, Object[][] inPointArray, int inAltFormat)
        {
+               if (inFieldArray == null || inPointArray == null)
+               {
+                       _numPoints = 0;
+                       return;
+               }
                // copy field list
                _masterFieldList = new FieldList(inFieldArray);
                // make DataPoint object from each point in inPointList
@@ -148,7 +153,8 @@ public class Track
                for (int i=0; i<_numPoints; i++)
                {
                        boolean keepPoint = true;
-                       if (!_dataPoints[i].isWaypoint())
+                       // Don't delete waypoints or photo points
+                       if (!_dataPoints[i].isWaypoint() && _dataPoints[i].getPhoto() == null)
                        {
                                // go through newPointArray to check for range
                                for (int j=0; j<numCopied && keepPoint; j++)
@@ -223,7 +229,6 @@ public class Track
         */
        public boolean deleteRange(int inStart, int inEnd)
        {
-               // TODO: Check for deleting photos?
                if (inStart < 0 || inEnd < 0 || inEnd < inStart)
                {
                        // no valid range selected so can't delete
@@ -243,7 +248,7 @@ public class Track
                        System.arraycopy(_dataPoints, inEnd + 1, newPointArray, inStart,
                                _numPoints - inEnd - 1);
                }
-               // Copy points over original array (careful!)
+               // Copy points over original array
                _dataPoints = newPointArray;
                _numPoints -= numToDelete;
                // needs to be scaled again
@@ -438,7 +443,6 @@ public class Track
                return true;
        }
 
-       // TODO: Need to rearrange photo points too?
 
        /**
         * Interpolate extra points between two selected ones
@@ -614,13 +618,12 @@ public class Track
                // loop over points and copy all waypoints into list
                for (int i=0; i<=_numPoints-1; i++)
                {
-                       if (_dataPoints[i].isWaypoint())
+                       if (_dataPoints[i] != null && _dataPoints[i].isWaypoint())
                        {
                                inList.add(_dataPoints[i]);
                        }
                }
        }
-       // TODO: Make similar method to get list of photos
 
 
        /**
@@ -869,14 +872,24 @@ public class Track
        {
                if (inPoint != null && inEditList != null && inEditList.getNumEdits() > 0)
                {
+                       // remember if coordinates have changed
+                       boolean coordsChanged = false;
                        // go through edits one by one
                        int numEdits = inEditList.getNumEdits();
                        for (int i=0; i<numEdits; i++)
                        {
                                FieldEdit edit = inEditList.getEdit(i);
                                inPoint.setFieldValue(edit.getField(), edit.getValue());
+                               // check coordinates
+                               coordsChanged |= (edit.getField().equals(Field.LATITUDE)
+                                       || edit.getField().equals(Field.LONGITUDE) || edit.getField().equals(Field.ALTITUDE));
+                       }
+                       // set photo status if coordinates have changed
+                       if (inPoint.getPhoto() != null && coordsChanged)
+                       {
+                               inPoint.getPhoto().setCurrentStatus(PhotoStatus.CONNECTED);
                        }
-                       // possibly needs to be scaled again
+                       // point possibly needs to be scaled again
                        _scaled = false;
                        // trigger listeners
                        _broker.informSubscribers();
index 4695d351e6c124c398e0df90c911dbe0583c4dbe..b987ea9449d6dff66fd52f2af60031752f415413 100644 (file)
@@ -14,7 +14,6 @@ public class TrackInfo
        private Track _track = null;
        private Selection _selection = null;
        private FileInfo _fileInfo = null;
-       // TODO: How to store photos? In separate list to be maintained or dynamic? Only store pointless photos?
        private PhotoList _photoList = null;
 
 
@@ -76,6 +75,15 @@ public class TrackInfo
                return _track.getPoint(_selection.getCurrentPointIndex());
        }
 
+       /**
+        * Get the currently selected photo, if any
+        * @return Photo if selected, otherwise null
+        */
+       public Photo getCurrentPhoto()
+       {
+               return _photoList.getPhoto(_selection.getCurrentPhotoIndex());
+       }
+
 
        /**
         * Load the specified data into the Track
@@ -94,12 +102,14 @@ public class TrackInfo
        /**
         * Add a List of Photos
         * @param inList List containing Photo objects
-        * @return number of photos added
+        * @return array containing number of photos and number of points added
         */
-       public int addPhotos(List inList)
+       public int[] addPhotos(List inList)
        {
-               // Firstly count number to add to make array
+               // TODO: Should photos be sorted at load-time, either by filename or date?
+               // Firstly count number of points and photos to add
                int numPhotosToAdd = 0;
+               int numPointsToAdd = 0;
                if (inList != null && !inList.isEmpty())
                {
                        for (int i=0; i<inList.size(); i++)
@@ -110,6 +120,10 @@ public class TrackInfo
                                        if (photo != null && !_photoList.contains(photo))
                                        {
                                                numPhotosToAdd++;
+                                               if (photo.getDataPoint() != null)
+                                               {
+                                                       numPointsToAdd++;
+                                               }
                                        }
                                }
                                catch (ClassCastException ce) {}
@@ -118,8 +132,9 @@ public class TrackInfo
                // If there are any photos to add, add them
                if (numPhotosToAdd > 0)
                {
-                       DataPoint[] dataPoints = new DataPoint[numPhotosToAdd];
+                       DataPoint[] dataPoints = new DataPoint[numPointsToAdd];
                        int pointNum = 0;
+                       boolean hasAltitude = false;
                        // Add each Photo in turn
                        for (int i=0; i<inList.size(); i++)
                        {
@@ -128,16 +143,32 @@ public class TrackInfo
                                        Photo photo = (Photo) inList.get(i);
                                        if (photo != null && !_photoList.contains(photo))
                                        {
+                                               // Add photo
                                                _photoList.addPhoto(photo);
-                                               dataPoints[pointNum] = photo.getDataPoint();
-                                               pointNum++;
+                                               // Add point if there is one
+                                               if (photo.getDataPoint() != null)
+                                               {
+                                                       dataPoints[pointNum] = photo.getDataPoint();
+                                                       // Check if any points have altitudes
+                                                       hasAltitude |= (photo.getDataPoint().getAltitude() != null);
+                                                       pointNum++;
+                                               }
                                        }
                                }
                                catch (ClassCastException ce) {}
                        }
-                       _track.appendPoints(dataPoints);
+                       if (numPointsToAdd > 0)
+                       {
+                               // add points to track
+                               _track.appendPoints(dataPoints);
+                               // modify track field list
+                               _track.getFieldList().extendList(Field.LATITUDE);
+                               _track.getFieldList().extendList(Field.LONGITUDE);
+                               if (hasAltitude) {_track.getFieldList().extendList(Field.ALTITUDE);}
+                       }
                }
-               return numPhotosToAdd;
+               int[] result = {numPhotosToAdd, numPointsToAdd};
+               return result;
        }
 
 
@@ -147,8 +178,6 @@ public class TrackInfo
         */
        public boolean deleteRange()
        {
-               // TODO: Check whether to delete photos associated with this range
-               int currPoint = _selection.getCurrentPointIndex();
                int startSel = _selection.getStart();
                int endSel = _selection.getEnd();
                boolean answer = _track.deleteRange(startSel, endSel);
@@ -166,7 +195,6 @@ public class TrackInfo
        {
                if (_track.deletePoint(_selection.getCurrentPointIndex()))
                {
-                       // TODO: Check whether to delete photo associated with this point
                        _selection.modifyPointDeleted();
                        _broker.informSubscribers();
                        return true;
@@ -175,6 +203,43 @@ public class TrackInfo
        }
 
 
+       /**
+        * Delete the currently selected photo and optionally its point too
+        * @param inPointToo true to also delete associated point
+        * @return true if delete successful
+        */
+       public boolean deleteCurrentPhoto(boolean inPointToo)
+       {
+               // delete currently selected photo
+               int photoIndex = _selection.getCurrentPhotoIndex();
+               if (photoIndex >= 0)
+               {
+                       Photo photo = _photoList.getPhoto(photoIndex);
+                       _photoList.deletePhoto(photoIndex);
+                       // has it got a point?
+                       if (photo.getDataPoint() != null)
+                       {
+                               if (inPointToo)
+                               {
+                                       // delete point
+                                       int pointIndex = _track.getPointIndex(photo.getDataPoint());
+                                       _track.deletePoint(pointIndex);
+                               }
+                               else
+                               {
+                                       // disconnect point from photo
+                                       photo.getDataPoint().setPhoto(null);
+                                       photo.setDataPoint(null);
+                               }
+                       }
+                       // update subscribers
+                       _selection.modifyPointDeleted();
+                       _broker.informSubscribers();
+               }
+               return true;
+       }
+
+
        /**
         * Compress the track to the given resolution
         * @param inResolution resolution
@@ -238,4 +303,35 @@ public class TrackInfo
                // give to selection
                _selection.selectPoint(index);
        }
+
+       /**
+        * Select the given Photo and its point if any
+        * @param inPhotoIndex index of photo to select
+        */
+       public void selectPhoto(int inPhotoIndex)
+       {
+               // Find Photo object
+               Photo photo = _photoList.getPhoto(inPhotoIndex);
+               if (photo != null)
+               {
+                       // Find point object and its index
+                       int pointIndex = _track.getPointIndex(photo.getDataPoint());
+                       // give to selection object
+                       _selection.selectPhotoAndPoint(inPhotoIndex, pointIndex);
+               }
+               else
+               {
+                       // no photo, just reset selection
+                       _selection.selectPhotoAndPoint(-1, -1);
+               }
+       }
+
+
+       /**
+        * Fire a trigger to all data subscribers
+        */
+       public void triggerUpdate()
+       {
+               _broker.informSubscribers();
+       }
 }
index bf66aaf0aa5f8dfe83455c9d71131055f64d2c09..3229bc29a00653f778315942b0cccb4849fb2d10 100644 (file)
@@ -20,7 +20,6 @@ import tim.prune.App;
 import tim.prune.I18nManager;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
-import tim.prune.data.Track;
 
 /**
  * Class to manage the display and editing of waypoint names
@@ -30,7 +29,6 @@ public class PointNameEditor
        private App _app = null;
        private JFrame _parentFrame = null;
        private JDialog _dialog = null;
-       private Track _track = null;
        private DataPoint _point = null;
        private JTextField _nameField = null;
        private JButton _okButton = null;
@@ -50,12 +48,10 @@ public class PointNameEditor
 
        /**
         * Show the edit point name dialog
-        * @param inTrack track object
         * @param inPoint point to edit
         */
-       public void showDialog(Track inTrack, DataPoint inPoint)
+       public void showDialog(DataPoint inPoint)
        {
-               _track = inTrack;
                _point = inPoint;
                _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.pointnameedit.title"), true);
                _dialog.setLocationRelativeTo(_parentFrame);
@@ -79,15 +75,34 @@ public class PointNameEditor
                panel.setLayout(new BorderLayout());
                // Create GUI layout for point name editor
                JPanel centrePanel = new JPanel();
-               // centrePanel.set
                centrePanel.add(new JLabel(I18nManager.getText("dialog.pointnameedit.name") + ":"));
+               // Make listener to react to ok being pressed
+               ActionListener okActionListener = new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // Check for empty name
+                               if (_nameField.getText().length() > 0)
+                               {
+                                       // update App with edit
+                                       confirmEdit();
+                                       _dialog.dispose();
+                               }
+                       }
+               };
                _nameField = new JTextField(inName, 12);
                _nameField.addKeyListener(new KeyAdapter() {
-                       public void keyTyped(KeyEvent e)
+                       public void keyReleased(KeyEvent e)
                        {
-                               _okButton.setEnabled(true);
+                               // close dialog if escape pressed
+                               if (e.getKeyCode() == KeyEvent.VK_ESCAPE)
+                               {
+                                       _dialog.dispose();
+                               }
+                               // Enable ok button if name not empty
+                               _okButton.setEnabled(_nameField.getText().length() > 0);
                        }
                });
+               _nameField.addActionListener(okActionListener);
                centrePanel.add(_nameField);
                panel.add(centrePanel);
                JPanel rightPanel = new JPanel();
@@ -98,6 +113,7 @@ public class PointNameEditor
                        {
                                _nameField.setText(_nameField.getText().toUpperCase());
                                _okButton.setEnabled(true);
+                               _nameField.requestFocus();
                        }
                });
                rightPanel.add(upperButton);
@@ -107,6 +123,7 @@ public class PointNameEditor
                        {
                                _nameField.setText(_nameField.getText().toLowerCase());
                                _okButton.setEnabled(true);
+                               _nameField.requestFocus();
                        }
                });
                rightPanel.add(lowerButton);
@@ -116,6 +133,7 @@ public class PointNameEditor
                        {
                                _nameField.setText(sentenceCase(_nameField.getText()));
                                _okButton.setEnabled(true);
+                               _nameField.requestFocus();
                        }
                });
                rightPanel.add(sentenceButton);
@@ -133,14 +151,7 @@ public class PointNameEditor
                lowerPanel.add(cancelButton);
                _okButton = new JButton(I18nManager.getText("button.ok"));
                _okButton.setEnabled(false);
-               _okButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               // update App with edit
-                               confirmEdit();
-                               _dialog.dispose();
-                       }
-               });
+               _okButton.addActionListener(okActionListener);
                lowerPanel.add(_okButton);
                panel.add(lowerPanel, BorderLayout.SOUTH);
                return panel;
index 09ce454ab056bafdb6b8bc04b4097226a5600d55..1e4f4f5bf5f98f04693f6657d5f460b29999fcc2 100644 (file)
@@ -1,9 +1,15 @@
 package tim.prune.gui;
 
+import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.FlowLayout;
 import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
 
 import javax.swing.BorderFactory;
 import javax.swing.BoxLayout;
@@ -13,15 +19,19 @@ import javax.swing.JEditorPane;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
 
+import tim.prune.ExternalTools;
 import tim.prune.GpsPruner;
 import tim.prune.I18nManager;
+import tim.prune.threedee.WindowFactory;
 
 /**
  * Class to represent the "About" popup window
  */
 public class AboutScreen extends JDialog
 {
+       JButton _okButton = null;
 
        /**
         * Constructor
@@ -39,19 +49,25 @@ public class AboutScreen extends JDialog
        private Component makeContents()
        {
                JPanel mainPanel = new JPanel();
-               mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
-               mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+               mainPanel.setLayout(new BorderLayout());
+
+               JTabbedPane tabPane = new JTabbedPane();
+               mainPanel.add(tabPane, BorderLayout.CENTER);
+
+               JPanel aboutPanel = new JPanel();
+               aboutPanel.setLayout(new BoxLayout(aboutPanel, BoxLayout.Y_AXIS));
+               aboutPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
                JLabel titleLabel = new JLabel("Prune");
                titleLabel.setFont(new Font("SansSerif", Font.BOLD, 24));
                titleLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
-               mainPanel.add(titleLabel);
+               aboutPanel.add(titleLabel);
                JLabel versionLabel = new JLabel(I18nManager.getText("dialog.about.version") + ": " + GpsPruner.VERSION_NUMBER);
                versionLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
-               mainPanel.add(versionLabel);
+               aboutPanel.add(versionLabel);
                JLabel buildLabel = new JLabel(I18nManager.getText("dialog.about.build") + ": " + GpsPruner.BUILD_NUMBER);
                buildLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
-               mainPanel.add(buildLabel);
-               mainPanel.add(new JLabel(" "));
+               aboutPanel.add(buildLabel);
+               aboutPanel.add(new JLabel(" "));
                StringBuffer descBuffer = new StringBuffer();
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.summarytext1")).append("</p>");
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.summarytext2")).append("</p>");
@@ -63,21 +79,147 @@ public class AboutScreen extends JDialog
                descPane.setOpaque(false);
                descPane.setAlignmentX(JEditorPane.CENTER_ALIGNMENT);
 
-               mainPanel.add(descPane);
-               mainPanel.add(new JLabel(" "));
-               JButton okButton = new JButton(I18nManager.getText("button.ok"));
-               okButton.addActionListener(new ActionListener()
+               aboutPanel.add(descPane);
+               aboutPanel.add(new JLabel(" "));
+               tabPane.add(I18nManager.getText("dialog.about.title"), aboutPanel);
+
+               // Second pane for system info
+               JPanel sysInfoPanel = new JPanel();
+               GridBagLayout gridBag = new GridBagLayout();
+               sysInfoPanel.setLayout(gridBag);
+               GridBagConstraints constraints = new GridBagConstraints();
+               constraints.weightx = 0.0; constraints.weighty = 0.0;
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.systeminfo.os") + " : "),
+                       0, 0);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(System.getProperty("os.name")),
+                       1, 0);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.systeminfo.java") + " : "),
+                       0, 1);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(System.getProperty("java.runtime.version")),
+                       1, 1);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.systeminfo.java3d") + " : "),
+                       0, 2);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText(WindowFactory.isJava3dEnabled()?"dialog.about.yes":"dialog.about.no")),
+                       1, 2);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.systeminfo.povray") + " : "),
+                       0, 3);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText(ExternalTools.isPovrayInstalled()?"dialog.about.yes":"dialog.about.no")),
+                       1, 3);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.systeminfo.exiftool") + " : "),
+                       0, 4);
+               addToGridBagPanel(sysInfoPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText(ExternalTools.isExiftoolInstalled()?"dialog.about.yes":"dialog.about.no")),
+                       1, 4);
+               tabPane.add(I18nManager.getText("dialog.about.systeminfo"), sysInfoPanel);
+
+               // Third pane for credits
+               JPanel creditsPanel = new JPanel();
+               gridBag = new GridBagLayout();
+               creditsPanel.setLayout(gridBag);
+               constraints = new GridBagConstraints();
+               constraints.weightx = 0.0; constraints.weighty = 0.0;
+
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.code") + " : "),
+                       0, 0);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("activityworkshop.net"),
+                       1, 0);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.exifcode") + " : "),
+                       0, 1);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Drew Noakes"),
+                       1, 1);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.icons") + " : "),
+                       0, 2);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Eclipse"),
+                       1, 2);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "),
+                       0, 3);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Open Office, Gpsdrive, Babelfish, Leo"),
+                       1, 3);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "),
+                       0, 4);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"),
+                       1, 4);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "),
+                       0, 5);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Garble, Kate, Povray, Inkscape, Google Earth"),
+                       1, 5);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "),
+                       0, 6);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Friends and loved ones, for encouragement and support"),
+                       1, 6);
+               tabPane.add(I18nManager.getText("dialog.about.credits"), creditsPanel);
+
+               // OK button at the bottom
+               JPanel okPanel = new JPanel();
+               okPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
+               _okButton = new JButton(I18nManager.getText("button.ok"));
+               _okButton.addActionListener(new ActionListener()
                {
                        public void actionPerformed(ActionEvent e)
                        {
                                dispose();
                        }
                });
-               okButton.setAlignmentX(JButton.CENTER_ALIGNMENT);
-               mainPanel.add(okButton);
+               _okButton.addKeyListener(new KeyListener() {
+                       public void keyPressed(KeyEvent e)
+                       {
+                               if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {dispose();}
+                       }
+                       public void keyTyped(KeyEvent e) {}
+                       public void keyReleased(KeyEvent e) {}
+               });
+               okPanel.add(_okButton);
+               mainPanel.add(okPanel, BorderLayout.SOUTH);
                return mainPanel;
        }
 
+       /**
+        * Helper function to reduce complexity of gui making code
+        * when adding labels to a GridBagLayout
+        * @param inPanel panel to add to
+        * @param inLayout GridBagLayout object
+        * @param inConstraints GridBagConstraints object
+        * @param inLabel label to add
+        * @param inX grid x
+        * @param inY grid y
+        */
+       private static void addToGridBagPanel(JPanel inPanel, GridBagLayout inLayout, GridBagConstraints inConstraints,
+               JLabel inLabel, int inX, int inY)
+       {
+               // set x and y in constraints
+               inConstraints.gridx = inX;
+               inConstraints.gridy = inY;
+               // set anchor
+               inConstraints.anchor = (inX == 0?GridBagConstraints.EAST:GridBagConstraints.WEST);
+               // set constraints to label
+               inLayout.setConstraints(inLabel, inConstraints);
+               // add label to panel
+               inPanel.add(inLabel);
+       }
+
 
        /**
         * Show window
@@ -87,5 +229,6 @@ public class AboutScreen extends JDialog
                pack();
                // setSize(300,200);
                super.show();
+               _okButton.requestFocus();
        }
 }
index 765b88b30c37f5206e6abb30ac676c5d850ce885..1c686262dff5b979a01ab4d1a590efe8546fc54d 100644 (file)
@@ -2,31 +2,19 @@ package tim.prune.gui;
 
 import java.awt.BorderLayout;
 import java.awt.Component;
-import java.awt.FlowLayout;
+import java.awt.Dimension;
 import java.awt.Font;
-import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.AdjustmentEvent;
-import java.awt.event.AdjustmentListener;
 import java.text.NumberFormat;
 
 import javax.swing.BorderFactory;
 import javax.swing.Box;
 import javax.swing.BoxLayout;
-import javax.swing.JButton;
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
-import javax.swing.JList;
 import javax.swing.JPanel;
-import javax.swing.JScrollBar;
-import javax.swing.JScrollPane;
-import javax.swing.JTabbedPane;
 import javax.swing.border.EtchedBorder;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-
-import tim.prune.App;
 import tim.prune.DataSubscriber;
 import tim.prune.I18nManager;
 import tim.prune.data.Altitude;
@@ -34,6 +22,7 @@ import tim.prune.data.Coordinate;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Distance;
 import tim.prune.data.IntegerRange;
+import tim.prune.data.Photo;
 import tim.prune.data.Selection;
 import tim.prune.data.TrackInfo;
 
@@ -43,34 +32,21 @@ import tim.prune.data.TrackInfo;
  */
 public class DetailsDisplay extends GenericDisplay
 {
-       // App object to be notified of editing commands
-       private App _app = null;
-
-       // Track details
-       private JLabel _trackpointsLabel = null;
-       private JLabel _filenameLabel = null;
        // Point details
        private JLabel _indexLabel = null;
        private JLabel _latLabel = null, _longLabel = null;
        private JLabel _altLabel = null, _nameLabel = null;
-       private JLabel _timeLabel = null, _photoFileLabel = null;
-       // Scroll bar
-       private JScrollBar _scroller = null;
-       private boolean _ignoreScrollEvents = false;
-       // Button panel
-       private JButton _startRangeButton = null, _endRangeButton = null;
-       private JButton _deletePointButton = null, _deleteRangeButton = null;
+       private JLabel _timeLabel = null;
 
        // Range details
        private JLabel _rangeLabel = null;
        private JLabel _distanceLabel = null, _durationLabel = null;
        private JLabel _altRangeLabel = null, _updownLabel = null;
-       // Photos
-       private JList _photoList = null;
-       private PhotoListModel _photoListModel = null;
-       // Waypoints
-       private JList _waypointList = null;
-       private WaypointListModel _waypointListModel = null;
+
+       // Photo details
+       private JLabel _photoLabel = null;
+       private PhotoThumbnail _photoThumbnail = null;
+
        // Units
        private JComboBox _unitsDropdown = null;
        // Formatter
@@ -91,39 +67,20 @@ public class DetailsDisplay extends GenericDisplay
        private static final String LABEL_RANGE_DESCENT = ", " + I18nManager.getText("details.range.descent") + ": ";
        private static String LABEL_POINT_ALTITUDE_UNITS = null;
        private static int LABEL_POINT_ALTITUDE_FORMAT = Altitude.FORMAT_NONE;
-       // scrollbar interval
-       private static final int SCROLLBAR_INTERVAL = 50;
 
 
        /**
         * Constructor
-        * @param inApp App object for callbacks
         * @param inTrackInfo Track info object
         */
-       public DetailsDisplay(App inApp, TrackInfo inTrackInfo)
+       public DetailsDisplay(TrackInfo inTrackInfo)
        {
                super(inTrackInfo);
-               _app = inApp;
                setLayout(new BorderLayout());
 
                JPanel mainPanel = new JPanel();
                mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
                mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
-               // Track details panel
-               JPanel trackDetailsPanel = new JPanel();
-               trackDetailsPanel.setLayout(new BoxLayout(trackDetailsPanel, BoxLayout.Y_AXIS));
-               trackDetailsPanel.setBorder(BorderFactory.createCompoundBorder(
-                       BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
-               );
-               JLabel trackDetailsLabel = new JLabel(I18nManager.getText("details.trackdetails"));
-               Font biggerFont = trackDetailsLabel.getFont();
-               biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f);
-               trackDetailsLabel.setFont(biggerFont);
-               trackDetailsPanel.add(trackDetailsLabel);
-               _trackpointsLabel = new JLabel(I18nManager.getText("details.notrack"));
-               trackDetailsPanel.add(_trackpointsLabel);
-               _filenameLabel = new JLabel("");
-               trackDetailsPanel.add(_filenameLabel);
 
                // Point details panel
                JPanel pointDetailsPanel = new JPanel();
@@ -132,6 +89,8 @@ public class DetailsDisplay extends GenericDisplay
                        BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
                );
                JLabel pointDetailsLabel = new JLabel(I18nManager.getText("details.pointdetails"));
+               Font biggerFont = pointDetailsLabel.getFont();
+               biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f);
                pointDetailsLabel.setFont(biggerFont);
                pointDetailsPanel.add(pointDetailsLabel);
                _indexLabel = new JLabel(I18nManager.getText("details.nopointselection"));
@@ -144,138 +103,63 @@ public class DetailsDisplay extends GenericDisplay
                pointDetailsPanel.add(_altLabel);
                _timeLabel = new JLabel("");
                pointDetailsPanel.add(_timeLabel);
-               _photoFileLabel = new JLabel("");
-               pointDetailsPanel.add(_photoFileLabel);
                _nameLabel = new JLabel("");
                pointDetailsPanel.add(_nameLabel);
                pointDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
 
-               // Scroll bar
-               _scroller = new JScrollBar(JScrollBar.HORIZONTAL, 0, SCROLLBAR_INTERVAL, 0, 100);
-               _scroller.addAdjustmentListener(new AdjustmentListener() {
-                       public void adjustmentValueChanged(AdjustmentEvent e)
-                       {
-                               selectPoint(e.getValue());
-                       }
-               });
-               _scroller.setEnabled(false);
-
-               // Button panel
-               JPanel buttonPanel = new JPanel();
-               buttonPanel.setLayout(new GridLayout(2, 2, 3, 3));
-               _startRangeButton = new JButton(I18nManager.getText("button.startrange"));
-               _startRangeButton.addActionListener(new ActionListener()
-                       {
-                               public void actionPerformed(ActionEvent e)
-                               {
-                                       _trackInfo.getSelection().selectRangeStart();
-                               }
-                       });
-               _startRangeButton.setEnabled(false);
-               buttonPanel.add(_startRangeButton);
-               _endRangeButton = new JButton(I18nManager.getText("button.endrange"));
-               _endRangeButton.addActionListener(new ActionListener()
-                       {
-                               public void actionPerformed(ActionEvent e)
-                               {
-                                       _trackInfo.getSelection().selectRangeEnd();
-                               }
-                       });
-               _endRangeButton.setEnabled(false);
-               buttonPanel.add(_endRangeButton);
-               _deletePointButton = new JButton(I18nManager.getText("button.deletepoint"));
-               _deletePointButton.addActionListener(new ActionListener()
-                       {
-                               public void actionPerformed(ActionEvent e)
-                               {
-                                       _app.deleteCurrentPoint();
-                               }
-                       });
-               _deletePointButton.setEnabled(false);
-               buttonPanel.add(_deletePointButton);
-               _deleteRangeButton = new JButton(I18nManager.getText("button.deleterange"));
-               _deleteRangeButton.addActionListener(new ActionListener()
-                       {
-                               public void actionPerformed(ActionEvent e)
-                               {
-                                       _app.deleteSelectedRange();
-                               }
-                       });
-               _deleteRangeButton.setEnabled(false);
-               buttonPanel.add(_deleteRangeButton);
-               buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
-
                // range details panel
-               JPanel otherDetailsPanel = new JPanel();
-               otherDetailsPanel.setLayout(new BoxLayout(otherDetailsPanel, BoxLayout.Y_AXIS));
-               otherDetailsPanel.setBorder(BorderFactory.createCompoundBorder(
+               JPanel rangeDetailsPanel = new JPanel();
+               rangeDetailsPanel.setLayout(new BoxLayout(rangeDetailsPanel, BoxLayout.Y_AXIS));
+               rangeDetailsPanel.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
                );
-
-               JLabel otherDetailsLabel = new JLabel(I18nManager.getText("details.rangedetails"));
-               otherDetailsLabel.setFont(biggerFont);
-               otherDetailsPanel.add(otherDetailsLabel);
+               JLabel rangeDetailsLabel = new JLabel(I18nManager.getText("details.rangedetails"));
+               rangeDetailsLabel.setFont(biggerFont);
+               rangeDetailsPanel.add(rangeDetailsLabel);
                _rangeLabel = new JLabel(I18nManager.getText("details.norangeselection"));
-               otherDetailsPanel.add(_rangeLabel);
+               rangeDetailsPanel.add(_rangeLabel);
                _distanceLabel = new JLabel("");
-               otherDetailsPanel.add(_distanceLabel);
+               rangeDetailsPanel.add(_distanceLabel);
                _durationLabel = new JLabel("");
-               otherDetailsPanel.add(_durationLabel);
+               rangeDetailsPanel.add(_durationLabel);
                _altRangeLabel = new JLabel("");
-               otherDetailsPanel.add(_altRangeLabel);
+               rangeDetailsPanel.add(_altRangeLabel);
                _updownLabel = new JLabel("");
-               otherDetailsPanel.add(_updownLabel);
-               otherDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+               rangeDetailsPanel.add(_updownLabel);
+               rangeDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
 
-               // Add tab panel for waypoints / photos
-               JPanel waypointsPanel = new JPanel();
-               waypointsPanel.setLayout(new BoxLayout(waypointsPanel, BoxLayout.Y_AXIS));
-               waypointsPanel.setBorder(BorderFactory.createCompoundBorder(
+               // range details panel
+               JPanel photoDetailsPanel = new JPanel();
+               photoDetailsPanel.setLayout(new BoxLayout(photoDetailsPanel, BoxLayout.Y_AXIS));
+               photoDetailsPanel.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
                );
-               JTabbedPane tabPane = new JTabbedPane();
-               _waypointListModel = new WaypointListModel(_trackInfo.getTrack());
-               _waypointList = new JList(_waypointListModel);
-               _waypointList.setVisibleRowCount(5);
-               _waypointList.addListSelectionListener(new ListSelectionListener() {
-                       public void valueChanged(ListSelectionEvent e)
-                       {
-                               if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex());
-                       }});
-               tabPane.addTab(I18nManager.getText("details.waypointsphotos.waypoints"), new JScrollPane(_waypointList));
-               _photoListModel = new PhotoListModel(_trackInfo.getPhotoList());
-               _photoList = new JList(_photoListModel);
-               _photoList.setVisibleRowCount(5);
-               _photoList.addListSelectionListener(new ListSelectionListener() {
-                       public void valueChanged(ListSelectionEvent e)
-                       {
-                               if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex());
-                       }});
-               // TODO: Re-add photos list after v2
-               // tabPane.addTab(I18nManager.getText("details.waypointsphotos.photos"), new JScrollPane(_photoList));
-               tabPane.setAlignmentX(Component.LEFT_ALIGNMENT);
-               waypointsPanel.add(tabPane);
-               waypointsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
-
-               // add the slider, point details, and the other details to the main panel
-               mainPanel.add(buttonPanel);
-               mainPanel.add(Box.createVerticalStrut(5));
-               mainPanel.add(_scroller);
-               mainPanel.add(Box.createVerticalStrut(5));
-               mainPanel.add(trackDetailsPanel);
-               mainPanel.add(Box.createVerticalStrut(5));
+               JLabel photoDetailsLabel = new JLabel(I18nManager.getText("details.photodetails"));
+               photoDetailsLabel.setFont(biggerFont);
+               photoDetailsPanel.add(photoDetailsLabel);
+               _photoLabel = new JLabel(I18nManager.getText("details.nophoto"));
+               photoDetailsPanel.add(_photoLabel);
+               _photoThumbnail = new PhotoThumbnail();
+               _photoThumbnail.setVisible(false);
+               _photoThumbnail.setPreferredSize(new Dimension(100, 100));
+               photoDetailsPanel.add(_photoThumbnail);
+
+               // add the details panels to the main panel
                mainPanel.add(pointDetailsPanel);
                mainPanel.add(Box.createVerticalStrut(5));
-               mainPanel.add(otherDetailsPanel);
+               mainPanel.add(rangeDetailsPanel);
+               mainPanel.add(Box.createVerticalStrut(5));
+               mainPanel.add(photoDetailsPanel);
                mainPanel.add(Box.createVerticalStrut(5));
-               mainPanel.add(waypointsPanel);
                // add the main panel at the top
                add(mainPanel, BorderLayout.NORTH);
 
                // Add units selection
                JPanel lowerPanel = new JPanel();
-               lowerPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
-               lowerPanel.add(new JLabel(I18nManager.getText("details.distanceunits") + ": "));
+               lowerPanel.setLayout(new BoxLayout(lowerPanel, BoxLayout.Y_AXIS));
+               JLabel unitsLabel = new JLabel(I18nManager.getText("details.distanceunits") + ": ");
+               unitsLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+               lowerPanel.add(unitsLabel);
                String[] distUnits = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.miles")};
                _unitsDropdown = new JComboBox(distUnits);
                _unitsDropdown.addActionListener(new ActionListener() {
@@ -285,85 +169,16 @@ public class DetailsDisplay extends GenericDisplay
                        }
                });
                lowerPanel.add(_unitsDropdown);
+               _unitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
                add(lowerPanel, BorderLayout.SOUTH);
        }
 
 
-       /**
-        * Select the specified point
-        * @param inValue value to select
-        */
-       private void selectPoint(int inValue)
-       {
-               if (_track != null && !_ignoreScrollEvents)
-               {
-                       _trackInfo.getSelection().selectPoint(inValue);
-               }
-       }
-
-
-       /**
-        * Select the specified photo
-        * @param inPhotoIndex index of selected photo
-        */
-       private void selectPhoto(int inPhotoIndex)
-       {
-               if (_photoListModel.getPhoto(inPhotoIndex) != null)
-               {
-                       // TODO: Deselect the photo when another point is selected
-                       // TODO: show photo thumbnail
-                       // select associated point, if any
-                       DataPoint point = _photoListModel.getPhoto(inPhotoIndex).getDataPoint();
-                       if (point != null)
-                       {
-                               _trackInfo.selectPoint(point);
-                       }
-               }
-       }
-
-
-       /**
-        * Select the specified waypoint
-        * @param inWaypointIndex index of selected waypoint
-        */
-       private void selectWaypoint(int inWaypointIndex)
-       {
-               if (inWaypointIndex >= 0)
-               {
-                       _trackInfo.selectPoint(_waypointListModel.getWaypoint(inWaypointIndex));
-               }
-       }
-
-
        /**
         * Notification that Track has been updated
         */
        public void dataUpdated(byte inUpdateType)
        {
-               // Update track data
-               if (_track == null || _track.getNumPoints() <= 0)
-               {
-                       _trackpointsLabel.setText(I18nManager.getText("details.notrack"));
-                       _filenameLabel.setText("");
-               }
-               else
-               {
-                       _trackpointsLabel.setText(I18nManager.getText("details.track.points") + ": "
-                               + _track.getNumPoints());
-                       int numFiles = _trackInfo.getFileInfo().getNumFiles();
-                       if (numFiles == 1)
-                       {
-                               _filenameLabel.setText(I18nManager.getText("details.track.file") + ": "
-                                       + _trackInfo.getFileInfo().getFilename());
-                       }
-                       else if (numFiles > 1)
-                       {
-                               _filenameLabel.setText(I18nManager.getText("details.track.numfiles") + ": "
-                                       + numFiles);
-                       }
-                       else _filenameLabel.setText("");
-               }
-
                // Update current point data, if any
                DataPoint currentPoint = _trackInfo.getCurrentPoint();
                Selection selection = _trackInfo.getSelection();
@@ -375,7 +190,6 @@ public class DetailsDisplay extends GenericDisplay
                        _longLabel.setText("");
                        _altLabel.setText("");
                        _timeLabel.setText("");
-                       _photoFileLabel.setText("");
                        _nameLabel.setText("");
                }
                else
@@ -393,13 +207,6 @@ public class DetailsDisplay extends GenericDisplay
                                _timeLabel.setText(LABEL_POINT_TIMESTAMP + currentPoint.getTimestamp().getText());
                        else
                                _timeLabel.setText("");
-                       if (currentPoint.getPhoto() != null && currentPoint.getPhoto().getFile() != null)
-                       {
-                               _photoFileLabel.setText(I18nManager.getText("details.photofile") + ": "
-                                       + currentPoint.getPhoto().getFile().getName());
-                       }
-                       else
-                               _photoFileLabel.setText("");
                        String name = currentPoint.getWaypointName();
                        if (name != null && !name.equals(""))
                        {
@@ -408,30 +215,6 @@ public class DetailsDisplay extends GenericDisplay
                        else _nameLabel.setText("");
                }
 
-               // Update scroller settings
-               _ignoreScrollEvents = true;
-               if (_track == null || _track.getNumPoints() < 2)
-               {
-                       // careful to avoid event loops here
-                       // _scroller.setValue(0);
-                       _scroller.setEnabled(false);
-               }
-               else
-               {
-                       _scroller.setMaximum(_track.getNumPoints() + SCROLLBAR_INTERVAL);
-                       if (currentPointIndex >= 0)
-                               _scroller.setValue(currentPointIndex);
-                       _scroller.setEnabled(true);
-               }
-               _ignoreScrollEvents = false;
-
-               // Update button panel
-               boolean hasPoint = (_track != null && currentPointIndex >= 0);
-               _startRangeButton.setEnabled(hasPoint);
-               _endRangeButton.setEnabled(hasPoint);
-               _deletePointButton.setEnabled(hasPoint);
-               _deleteRangeButton.setEnabled(selection.hasRangeSelected());
-
                // Update range details
                if (_track == null || !selection.hasRangeSelected())
                {
@@ -475,37 +258,22 @@ public class DetailsDisplay extends GenericDisplay
                                _updownLabel.setText("");
                        }
                }
-               // update waypoints and photos if necessary
-               if ((inUpdateType |
-                       (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.WAYPOINTS_MODIFIED)) > 0)
-               {
-                       _waypointListModel.fireChanged();
-               }
-               if ((inUpdateType |
-                       (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.PHOTOS_MODIFIED)) > 0)
-               {
-                       _photoListModel.fireChanged();
-               }
-               // Deselect selected waypoint if selected point has since changed
-               if (_waypointList.getSelectedIndex() >= 0)
+               // show photo details and thumbnail
+               Photo currentPhoto = _trackInfo.getPhotoList().getPhoto(_trackInfo.getSelection().getCurrentPhotoIndex());
+               if (_track == null || ( (currentPoint == null || currentPoint.getPhoto() == null) && currentPhoto == null))
                {
-                       if (_trackInfo.getCurrentPoint() == null
-                        || !_waypointListModel.getWaypoint(_waypointList.getSelectedIndex()).equals(_trackInfo.getCurrentPoint()))
-                       {
-                               // point is selected in list but different from current point - deselect
-                               _waypointList.clearSelection();
-                       }
+                       // no photo, hide details
+                       _photoLabel.setText(I18nManager.getText("details.nophoto"));
+                       _photoThumbnail.setVisible(false);
                }
-               // Do the same for the photos
-               if (_photoList.getSelectedIndex() >= 0)
+               else
                {
-                       if (_trackInfo.getCurrentPoint() == null
-                               || !_photoListModel.getPhoto(_photoList.getSelectedIndex()).getDataPoint().equals(_trackInfo.getCurrentPoint()))
-                       {
-                               // photo is selected in list but different from current point - deselect
-                               _photoList.clearSelection();
-                       }
+                       if (currentPhoto == null) {currentPhoto = currentPoint.getPhoto();}
+                       _photoLabel.setText(I18nManager.getText("details.photofile") + ": " + currentPhoto.getFile().getName());
+                       _photoThumbnail.setVisible(true);
+                       _photoThumbnail.setPhoto(currentPhoto);
                }
+               _photoThumbnail.repaint();
        }
 
 
diff --git a/tim/prune/gui/ImageUtils.java b/tim/prune/gui/ImageUtils.java
new file mode 100644 (file)
index 0000000..4b46959
--- /dev/null
@@ -0,0 +1,86 @@
+package tim.prune.gui;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.ConvolveOp;
+import java.awt.image.Kernel;
+
+import javax.swing.ImageIcon;
+
+/**
+ * Class for providing generic image processing functions
+ */
+public abstract class ImageUtils
+{
+       private static final float SMOOTH_FACTOR = 0.008f;
+       private static ConvolveOp CONVOLVER = null;
+
+       /** Static block for initialization */
+       static
+       {
+               float[] smoothMatrix = {
+                       0, SMOOTH_FACTOR, 0,
+                       SMOOTH_FACTOR, 1-(SMOOTH_FACTOR*4), SMOOTH_FACTOR,
+                       0, SMOOTH_FACTOR, 0
+               };
+               CONVOLVER = new ConvolveOp(new Kernel(3, 3, smoothMatrix));
+       }
+
+
+       /**
+        * Create a scaled and smoothed image according to the specified size
+        * @param inImage image to scale
+        * @param inWidth width to scale to
+        * @param inHeight height to scale to
+        * @return BufferedImage containing scaled result
+        */
+       public static BufferedImage createScaledImage(Image inImage, int inWidth, int inHeight)
+       {
+               // create smaller image and force its loading
+               Image smallerImage = inImage.getScaledInstance(inWidth, inHeight, Image.SCALE_SMOOTH);
+               Image tempImage = new ImageIcon(smallerImage).getImage();
+               tempImage.getWidth(null);
+
+               // create buffered image to do transform
+               BufferedImage buffer = new BufferedImage(inWidth, inHeight, BufferedImage.TYPE_INT_RGB);
+               // copy scaled picture into buffer
+               Graphics buffG = buffer.getGraphics();
+               buffG.drawImage(smallerImage, 0, 0, inWidth, inHeight, null);
+               buffG.dispose();
+
+               // clear variables
+               smallerImage = null; tempImage = null;
+               // smooth scaled image using a normalized 3x3 matrix - taking next neighbour
+               buffer = CONVOLVER.filter(buffer, null);
+
+               return buffer;
+       }
+
+
+       /**
+        * Work out the max size of a thumbnail
+        * @param inOrigWidth width of original picture
+        * @param inOrigHeight height of original picture
+        * @param inMaxWidth max width of thumbnail
+        * @param inMaxHeight max height of thumbnail
+        * @return size of thumbnail as Dimension
+        */
+       public static Dimension getThumbnailSize(int inOrigWidth, int inOrigHeight, int inMaxWidth, int inMaxHeight)
+       {
+               if (inMaxWidth <= 0 || inMaxHeight <= 0)
+               {
+                       //System.out.println("Can't do it - maxwidth=" + inMaxWidth + ", maxheight=" + inMaxHeight);
+                       return new Dimension(0,0);
+               }
+               // work out maximum zoom ratio available so that thumbnail isn't too big
+               double xZoom = inMaxWidth * 1.0 / inOrigWidth;
+               double yZoom = inMaxHeight * 1.0 / inOrigHeight;
+               double zoom = (xZoom > yZoom?yZoom:xZoom);
+               // Don't make thumbnail bigger than picture
+               if (zoom > 1.0) {return new Dimension(inOrigWidth, inOrigHeight);}
+               // calculate new width and height
+               return new Dimension ((int) (zoom * inOrigWidth), (int) (zoom * inOrigHeight));
+       }
+}
index 1236f130558630c4278b7a8cc31762ec34630ad4..e705f843aab3c74eb688f173766d5bbb5e7c8820 100644 (file)
@@ -49,7 +49,6 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
        private BufferedImage _image = null;
        private JPopupMenu _popup = null;
        private JCheckBoxMenuItem _autoPanMenuItem = null;
-       private String _trackString = null;
        private int _numPoints = -1;
        private double _scale;
        private double _offsetX, _offsetY, _zoomScale;
@@ -153,10 +152,14 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                }
                _lastSelectedPoint = selectedPoint;
 
+               // Create background if necessary
                if (_image == null || width != _image.getWidth() || height != _image.getHeight())
                {
                        createBackgroundImage();
                }
+               // return if image has been set to null by other thread
+               if (_image == null) {return;}
+
                // draw buffered image onto g
                g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
 
@@ -208,7 +211,7 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                }
 
                // Attempt to grab keyboard focus if possible
-               this.requestFocus();
+               //this.requestFocus();
        }
 
 
@@ -250,8 +253,11 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                {
                        DataPoint point = _track.getPoint(i);
                        String waypointName = point.getWaypointName();
-                       if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES)
+                       if (waypointName != null && !waypointName.equals(""))
                        {
+                               // escape if nothing more to do
+                               if (numWaypointNamesShown >= LIMIT_WAYPOINT_NAMES || _image == null) {break;}
+                               // calculate coordinates of point
                                x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
                                y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
                                if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
@@ -305,15 +311,22 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
         */
        private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
        {
-               // if (true) return true;
-               for (int x=0; x<inWidth; x++)
+               try
                {
-                       for (int y=0; y<inHeight; y++)
+                       // loop over x coordinate of rectangle
+                       for (int x=0; x<inWidth; x++)
                        {
-                               int pixelColor = _image.getRGB(inX + x, inY - y);
-                               if (pixelColor != -1) return true;
+                               // loop over y coordinate of rectangle
+                               for (int y=0; y<inHeight; y++)
+                               {
+                                       int pixelColor = _image.getRGB(inX + x, inY - y);
+                                       if (pixelColor != -1) return true;
+                               }
                        }
                }
+               catch (NullPointerException e) {
+                       // ignore null pointers, just return false
+               }
                return false;
        }
 
index 64a66d86aaba94296fb04e24535916d3e95670b9..5a4b009ada4d092265b337cc72234fd76ad934e4 100644 (file)
@@ -5,22 +5,26 @@ import java.awt.event.ActionListener;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
 import javax.swing.JFrame;
 import javax.swing.JMenu;
 import javax.swing.JMenuBar;
 import javax.swing.JMenuItem;
+import javax.swing.JToolBar;
 import javax.swing.KeyStroke;
 
 import tim.prune.App;
 import tim.prune.DataSubscriber;
 import tim.prune.I18nManager;
+import tim.prune.data.PhotoList;
 import tim.prune.data.Selection;
 import tim.prune.data.Track;
 import tim.prune.data.TrackInfo;
 
 /**
- * Class to manage the menu bar,
- * including enabling and disabling the menu items
+ * Class to manage the menu bar and tool bar,
+ * including enabling and disabling the items
  */
 public class MenuManager implements DataSubscriber
 {
@@ -28,28 +32,53 @@ public class MenuManager implements DataSubscriber
        private App _app = null;
        private Track _track = null;
        private Selection _selection = null;
+       private PhotoList _photos = null;
 
        // Menu items which need enabling/disabling
-       JMenuItem _saveItem = null;
-       JMenuItem _exportKmlItem = null;
-       JMenuItem _exportPovItem = null;
-       JMenuItem _undoItem = null;
-       JMenuItem _clearUndoItem = null;
-       JMenuItem _editPointItem = null;
-       JMenuItem _editWaypointNameItem = null;
-       JMenuItem _deletePointItem = null;
-       JMenuItem _deleteRangeItem = null;
-       JMenuItem _deleteDuplicatesItem = null;
-       JMenuItem _compressItem = null;
-       JMenuItem _interpolateItem = null;
-       JMenuItem _selectAllItem = null;
-       JMenuItem _selectNoneItem = null;
-       JMenuItem _show3dItem = null;
-       JMenuItem _reverseItem = null;
-       JMenu     _rearrangeMenu = null;
-       JMenuItem _rearrangeStartItem = null;
-       JMenuItem _rearrangeEndItem = null;
-       JMenuItem _rearrangeNearestItem = null;
+       private JMenuItem _saveItem = null;
+       private JMenuItem _exportKmlItem = null;
+       private JMenuItem _exportPovItem = null;
+       private JMenuItem _undoItem = null;
+       private JMenuItem _clearUndoItem = null;
+       private JMenuItem _editPointItem = null;
+       private JMenuItem _editWaypointNameItem = null;
+       private JMenuItem _deletePointItem = null;
+       private JMenuItem _deleteRangeItem = null;
+       private JMenuItem _deleteDuplicatesItem = null;
+       private JMenuItem _compressItem = null;
+       private JMenuItem _interpolateItem = null;
+       private JMenuItem _selectAllItem = null;
+       private JMenuItem _selectNoneItem = null;
+       private JMenuItem _selectStartItem = null;
+       private JMenuItem _selectEndItem = null;
+       private JMenuItem _reverseItem = null;
+       private JMenu     _rearrangeMenu = null;
+       private JMenuItem _rearrangeStartItem = null;
+       private JMenuItem _rearrangeEndItem = null;
+       private JMenuItem _rearrangeNearestItem = null;
+       private JMenuItem _show3dItem = null;
+       private JMenuItem _saveExifItem = null;
+       private JMenuItem _connectPhotoItem = null;
+       private JMenuItem _deletePhotoItem = null;
+       // TODO: Does Photo menu require disconnect option?
+
+       // ActionListeners for reuse by menu and toolbar
+       private ActionListener _openFileAction = null;
+       private ActionListener _addPhotoAction = null;
+       private ActionListener _saveAction = null;
+       private ActionListener _undoAction = null;
+       private ActionListener _editPointAction = null;
+       private ActionListener _selectStartAction = null;
+       private ActionListener _selectEndAction = null;
+       private ActionListener _connectPhotoAction = null;
+
+       // Toolbar buttons which need enabling/disabling
+       private JButton _saveButton = null;
+       private JButton _undoButton = null;
+       private JButton _editPointButton = null;
+       private JButton _selectStartButton = null;
+       private JButton _selectEndButton = null;
+       private JButton _connectPhotoButton = null;
 
 
        /**
@@ -63,6 +92,7 @@ public class MenuManager implements DataSubscriber
                _app = inApp;
                _track = inTrackInfo.getTrack();
                _selection = inTrackInfo.getSelection();
+               _photos = inTrackInfo.getPhotoList();
        }
 
 
@@ -77,31 +107,33 @@ public class MenuManager implements DataSubscriber
                // Open file
                JMenuItem openMenuItem = new JMenuItem(I18nManager.getText("menu.file.open"));
                openMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
-               openMenuItem.addActionListener(new ActionListener() {
+               _openFileAction = new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                _app.openFile();
                        }
-               });
+               };
+               openMenuItem.addActionListener(_openFileAction);
                fileMenu.add(openMenuItem);
                // Add photos
                JMenuItem addPhotosMenuItem = new JMenuItem(I18nManager.getText("menu.file.addphotos"));
-               addPhotosMenuItem.addActionListener(new ActionListener() {
+               _addPhotoAction = new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                _app.addPhotos();
                        }
-               });
-               // TODO: Re-add add photos menu item after v2
-               // fileMenu.add(addPhotosMenuItem);
+               };
+               addPhotosMenuItem.addActionListener(_addPhotoAction);
+               fileMenu.add(addPhotosMenuItem);
                // Save
                _saveItem = new JMenuItem(I18nManager.getText("menu.file.save"), KeyEvent.VK_S);
-               _saveItem.addActionListener(new ActionListener() {
+               _saveAction = new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                _app.saveFile();
                        }
-               });
+               };
+               _saveItem.addActionListener(_saveAction);
                _saveItem.setEnabled(false);
                fileMenu.add(_saveItem);
                // Export
@@ -136,12 +168,13 @@ public class MenuManager implements DataSubscriber
                JMenu editMenu = new JMenu(I18nManager.getText("menu.edit"));
                editMenu.setMnemonic(KeyEvent.VK_E);
                _undoItem = new JMenuItem(I18nManager.getText("menu.edit.undo"));
-               _undoItem.addActionListener(new ActionListener() {
+               _undoAction = new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                _app.beginUndo();
                        }
-               });
+               };
+               _undoItem.addActionListener(_undoAction);
                _undoItem.setEnabled(false);
                editMenu.add(_undoItem);
                _clearUndoItem = new JMenuItem(I18nManager.getText("menu.edit.clearundo"));
@@ -155,12 +188,13 @@ public class MenuManager implements DataSubscriber
                editMenu.add(_clearUndoItem);
                editMenu.addSeparator();
                _editPointItem = new JMenuItem(I18nManager.getText("menu.edit.editpoint"));
-               _editPointItem.addActionListener(new ActionListener() {
+               _editPointAction = new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                _app.editCurrentPoint();
                        }
-               });
+               };
+               _editPointItem.addActionListener(_editPointAction);
                _editPointItem.setEnabled(false);
                editMenu.add(_editPointItem);
                _editWaypointNameItem = new JMenuItem(I18nManager.getText("menu.edit.editwaypointname"));
@@ -280,8 +314,64 @@ public class MenuManager implements DataSubscriber
                        }
                });
                selectMenu.add(_selectNoneItem);
+               selectMenu.addSeparator();
+               _selectStartItem = new JMenuItem(I18nManager.getText("menu.select.start"));
+               _selectStartItem.setEnabled(false);
+               _selectStartAction = new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _selection.selectRangeStart();
+                       }
+               };
+               _selectStartItem.addActionListener(_selectStartAction);
+               selectMenu.add(_selectStartItem);
+               _selectEndItem = new JMenuItem(I18nManager.getText("menu.select.end"));
+               _selectEndItem.setEnabled(false);
+               _selectEndAction = new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _selection.selectRangeEnd();
+                       }
+               };
+               _selectEndItem.addActionListener(_selectEndAction);
+               selectMenu.add(_selectEndItem);
                menubar.add(selectMenu);
 
+               // Add photo menu
+               JMenu photoMenu = new JMenu(I18nManager.getText("menu.photo"));
+               addPhotosMenuItem = new JMenuItem(I18nManager.getText("menu.file.addphotos"));
+               addPhotosMenuItem.addActionListener(_addPhotoAction);
+               photoMenu.add(addPhotosMenuItem);
+               _saveExifItem = new JMenuItem(I18nManager.getText("menu.photo.saveexif"));
+               _saveExifItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.saveExif();
+                       }
+               });
+               _saveExifItem.setEnabled(false);
+               photoMenu.add(_saveExifItem);
+               _connectPhotoItem = new JMenuItem(I18nManager.getText("menu.photo.connect"));
+               _connectPhotoAction = new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.connectPhotoToPoint();
+                       }
+               };
+               _connectPhotoItem.addActionListener(_connectPhotoAction);
+               _connectPhotoItem.setEnabled(false);
+               photoMenu.add(_connectPhotoItem);
+               _deletePhotoItem = new JMenuItem(I18nManager.getText("menu.photo.delete"));
+               _deletePhotoItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.deleteCurrentPhoto();
+                       }
+               });
+               _deletePhotoItem.setEnabled(false);
+               photoMenu.add(_deletePhotoItem);
+               menubar.add(photoMenu);
+
                // Add 3d menu (whether java3d available or not)
                JMenu threeDMenu = new JMenu(I18nManager.getText("menu.3d"));
                _show3dItem = new JMenuItem(I18nManager.getText("menu.3d.show3d"));
@@ -311,6 +401,63 @@ public class MenuManager implements DataSubscriber
        }
 
 
+       /**
+        * Create a JToolBar containing all toolbar buttons
+        * @return toolbar containing buttons
+        */
+       public JToolBar createToolBar()
+       {
+               JToolBar toolbar = new JToolBar();
+               // Add text file
+               JButton openFileButton = new JButton(new ImageIcon(getClass().getResource("images/add_textfile_icon.png")));
+               openFileButton.setToolTipText(I18nManager.getText("menu.file.open"));
+               openFileButton.addActionListener(_openFileAction);
+               toolbar.add(openFileButton);
+               // Add photo
+               JButton addPhotoButton = new JButton(new ImageIcon(getClass().getResource("images/add_photo_icon.png")));
+               addPhotoButton.setToolTipText(I18nManager.getText("menu.file.addphotos"));
+               addPhotoButton.addActionListener(_addPhotoAction);
+               toolbar.add(addPhotoButton);
+               // Save
+               _saveButton = new JButton(new ImageIcon(getClass().getResource("images/save_icon.gif")));
+               _saveButton.setToolTipText(I18nManager.getText("menu.file.save"));
+               _saveButton.addActionListener(_saveAction);
+               _saveButton.setEnabled(false);
+               toolbar.add(_saveButton);
+               // Undo
+               _undoButton = new JButton(new ImageIcon(getClass().getResource("images/undo_icon.gif")));
+               _undoButton.setToolTipText(I18nManager.getText("menu.edit.undo"));
+               _undoButton.addActionListener(_undoAction);
+               _undoButton.setEnabled(false);
+               toolbar.add(_undoButton);
+               // Edit point
+               _editPointButton = new JButton(new ImageIcon(getClass().getResource("images/edit_point_icon.gif")));
+               _editPointButton.setToolTipText(I18nManager.getText("menu.edit.editpoint"));
+               _editPointButton.addActionListener(_editPointAction);
+               _editPointButton.setEnabled(false);
+               toolbar.add(_editPointButton);
+               // Select start, end
+               _selectStartButton = new JButton(new ImageIcon(getClass().getResource("images/set_start_icon.png")));
+               _selectStartButton.setToolTipText(I18nManager.getText("menu.select.start"));
+               _selectStartButton.addActionListener(_selectStartAction);
+               _selectStartButton.setEnabled(false);
+               toolbar.add(_selectStartButton);
+               _selectEndButton = new JButton(new ImageIcon(getClass().getResource("images/set_end_icon.png")));
+               _selectEndButton.setToolTipText(I18nManager.getText("menu.select.end"));
+               _selectEndButton.addActionListener(_selectEndAction);
+               _selectEndButton.setEnabled(false);
+               toolbar.add(_selectEndButton);
+               _connectPhotoButton = new JButton(new ImageIcon(getClass().getResource("images/connect_photo_icon.png")));
+               _connectPhotoButton.setToolTipText(I18nManager.getText("menu.photo.connect"));
+               _connectPhotoButton.addActionListener(_connectPhotoAction);
+               _connectPhotoButton.setEnabled(false);
+               toolbar.add(_connectPhotoButton);
+               // finish off
+               toolbar.setFloatable(false);
+               return toolbar;
+       }
+
+
        /**
         * Method to update menu when file loaded
         */
@@ -332,6 +479,7 @@ public class MenuManager implements DataSubscriber
                boolean hasData = (_track != null && _track.getNumPoints() > 0);
                // set functions which require data
                _saveItem.setEnabled(hasData);
+               _saveButton.setEnabled(hasData);
                _exportKmlItem.setEnabled(hasData);
                _exportPovItem.setEnabled(hasData);
                _deleteDuplicatesItem.setEnabled(hasData);
@@ -344,12 +492,29 @@ public class MenuManager implements DataSubscriber
                // is undo available?
                boolean hasUndo = !_app.getUndoStack().isEmpty();
                _undoItem.setEnabled(hasUndo);
+               _undoButton.setEnabled(hasUndo);
                _clearUndoItem.setEnabled(hasUndo);
                // is there a current point?
                boolean hasPoint = (hasData && _selection.getCurrentPointIndex() >= 0);
                _editPointItem.setEnabled(hasPoint);
+               _editPointButton.setEnabled(hasPoint);
                _editWaypointNameItem.setEnabled(hasPoint);
                _deletePointItem.setEnabled(hasPoint);
+               _selectStartItem.setEnabled(hasPoint);
+               _selectStartButton.setEnabled(hasPoint);
+               _selectEndItem.setEnabled(hasPoint);
+               _selectEndButton.setEnabled(hasPoint);
+               // are there any photos?
+               _saveExifItem.setEnabled(_photos != null && _photos.getNumPhotos() > 0);
+               // is there a current photo?
+               boolean hasPhoto = _photos != null && _photos.getNumPhotos() > 0
+                       && _selection.getCurrentPhotoIndex() >= 0;
+               // connect is only available when current photo is not connected to current point
+               boolean connectAvailable = hasPhoto && hasPoint
+                       && _track.getPoint(_selection.getCurrentPointIndex()).getPhoto() == null;
+               _connectPhotoItem.setEnabled(connectAvailable);
+               _connectPhotoButton.setEnabled(connectAvailable);
+               _deletePhotoItem.setEnabled(hasPhoto);
                // is there a current range?
                boolean hasRange = (hasData && _selection.hasRangeSelected());
                _deleteRangeItem.setEnabled(hasRange);
diff --git a/tim/prune/gui/PhotoThumbnail.java b/tim/prune/gui/PhotoThumbnail.java
new file mode 100644 (file)
index 0000000..a8c94eb
--- /dev/null
@@ -0,0 +1,123 @@
+package tim.prune.gui;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+
+import javax.swing.ImageIcon;
+import javax.swing.JPanel;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Photo;
+
+/**
+ * GUI component for showing photo thumbnail
+ */
+public class PhotoThumbnail extends JPanel implements Runnable
+{
+       private Photo _photo = null;
+       private BufferedImage _thumbnail = null;
+       private int _lastWidth = -1;
+       private int _lastHeight = -1;
+       private boolean _loadingImage = false;
+       private static String _loadingString = null;
+
+
+       /**
+        * Constructor
+        */
+       public PhotoThumbnail()
+       {
+               // TODO: Make size of thumbnail dynamic, as big as it can be
+               setOpaque(true);
+               _loadingString = I18nManager.getText("details.photo.loading") + " ...";
+       }
+
+
+       /**
+        * Set the Photo
+        * @param inPhoto Photo object to show thumbnail for
+        */
+       public void setPhoto(Photo inPhoto)
+       {
+               // Check whether the photo has changed
+               if (_photo == inPhoto) {return;}
+               _photo = inPhoto;
+               _thumbnail = null;
+       }
+
+
+       /**
+        * Override paint method
+        * @see javax.swing.JComponent#paint(java.awt.Graphics)
+        */
+       public void paint(Graphics inG)
+       {
+               super.paint(inG);
+               if (_photo != null)
+               {
+                       // recalculate thumbnail if photo has changed
+                       if (_thumbnail == null || getWidth() != _lastWidth || getHeight() != _lastHeight)
+                       {
+                               // initiate load if not already started
+                               if (!_loadingImage)
+                               {
+                                       _loadingImage = true;
+                                       new Thread(this).start();
+                               }
+                       }
+                       // Set width and height
+                       _lastWidth = getWidth();
+                       _lastHeight = getHeight();
+                       // if loading, display image
+                       if (_loadingImage)
+                       {
+                               inG.setColor(Color.BLACK);
+                               inG.drawString(_loadingString, 10, 30);
+                       }
+                       else
+                       {
+                               // Copy scaled, smoothed image onto the screen
+                               inG.drawImage(_thumbnail, 0, 0, _thumbnail.getWidth(), _thumbnail.getHeight(), null);
+                       }
+               }
+       }
+
+
+       /**
+        * Run method, for loading image in separate thread
+        * @see java.lang.Runnable#run()
+        */
+       public void run()
+       {
+               int picWidth = _photo.getWidth();
+               int picHeight = _photo.getHeight();
+               if (picWidth > -1 && picHeight > -1)
+               {
+                       int displayWidth = Math.min(getWidth(), getParent().getWidth());
+                       // System.out.println("width = " + getWidth() + ", " + getParent().getWidth() + " = " + displayWidth);
+                       int displayHeight = Math.min(getHeight(), getParent().getHeight());
+                       // System.out.println("height = " + getHeight() + ", " + getParent().getHeight() + " = " + displayHeight);
+
+                       // calculate maximum thumbnail size
+                       Dimension thumbSize = ImageUtils.getThumbnailSize(picWidth, picHeight, displayWidth, displayHeight);
+                       // Work out if need to remake image
+                       boolean needToRemake = (_thumbnail == null)
+                        || _thumbnail.getWidth() != thumbSize.width || _thumbnail.getHeight() != thumbSize.height;
+                       if (thumbSize.width > 0 && thumbSize.height > 0 && needToRemake)
+                       {
+                               // Make icon to load image into
+                               Image image = new ImageIcon(_photo.getFile().getAbsolutePath()).getImage();
+                               // save scaled, smoothed thumbnail for reuse
+                               _thumbnail = ImageUtils.createScaledImage(image, thumbSize.width, thumbSize.height);
+                               image = null;
+                               // TODO: Calculate and set size of thumbnail here
+                               // setPreferredSize(new Dimension(200, 200));
+                       }
+               }
+               _loadingImage = false;
+               repaint();
+       }
+}
index 431da0d396f0a1217e65e8098423cc2febc858fb..8bc2c055a5301910b8c3a71cc1b3708204794078 100644 (file)
@@ -38,6 +38,7 @@ public class ProfileChart extends GenericChart
 
        /**
         * Override paint method to draw map
+        * @param g Graphics object
         */
        public void paint(Graphics g)
        {
@@ -51,7 +52,8 @@ public class ProfileChart extends GenericChart
                        int maxAltitude = altitudeRange.getMaximum();
 
                        // message if no altitudes in track
-                       if (minAltitude < 0 || maxAltitude < 0)
+                       if (minAltitude < 0 || maxAltitude < 0
+                               || minAltitude == maxAltitude)
                        {
                                g.setColor(COLOR_LINES);
                                g.drawString(I18nManager.getText("display.noaltitudes"), 50, height/2);
diff --git a/tim/prune/gui/SelectorDisplay.java b/tim/prune/gui/SelectorDisplay.java
new file mode 100644 (file)
index 0000000..38e4165
--- /dev/null
@@ -0,0 +1,259 @@
+package tim.prune.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollBar;
+import javax.swing.JScrollPane;
+import javax.swing.border.EtchedBorder;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import tim.prune.DataSubscriber;
+import tim.prune.I18nManager;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Photo;
+import tim.prune.data.TrackInfo;
+
+/**
+ * Class to allow selection of points and photos
+ * as a visual component
+ */
+public class SelectorDisplay extends GenericDisplay
+{
+       // Track details
+       private JLabel _trackpointsLabel = null;
+       private JLabel _filenameLabel = null;
+       // Scroll bar
+       private JScrollBar _scroller = null;
+       private boolean _ignoreScrollEvents = false;
+
+       // Photos
+       private JList _photoList = null;
+       private PhotoListModel _photoListModel = null;
+       // Waypoints
+       private JList _waypointList = null;
+       private WaypointListModel _waypointListModel = null;
+
+       // scrollbar interval
+       private static final int SCROLLBAR_INTERVAL = 50;
+       // number of rows in lists
+       private static final int NUM_LIST_ENTRIES = 7;
+
+
+       /**
+        * Constructor
+        * @param inTrackInfo Track info object
+        */
+       public SelectorDisplay(TrackInfo inTrackInfo)
+       {
+               super(inTrackInfo);
+               setLayout(new BorderLayout());
+
+               JPanel mainPanel = new JPanel();
+               mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+               mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
+
+               // Track details panel
+               JPanel trackDetailsPanel = new JPanel();
+               trackDetailsPanel.setLayout(new BoxLayout(trackDetailsPanel, BoxLayout.Y_AXIS));
+               trackDetailsPanel.setBorder(BorderFactory.createCompoundBorder(
+                       BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
+               );
+               JLabel trackDetailsLabel = new JLabel(I18nManager.getText("details.trackdetails"));
+               Font biggerFont = trackDetailsLabel.getFont();
+               biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f);
+               trackDetailsLabel.setFont(biggerFont);
+               trackDetailsPanel.add(trackDetailsLabel);
+               _trackpointsLabel = new JLabel(I18nManager.getText("details.notrack"));
+               trackDetailsPanel.add(_trackpointsLabel);
+               _filenameLabel = new JLabel("");
+               trackDetailsPanel.add(_filenameLabel);
+
+               // Scroll bar
+               _scroller = new JScrollBar(JScrollBar.HORIZONTAL, 0, SCROLLBAR_INTERVAL, 0, 100);
+               _scroller.addAdjustmentListener(new AdjustmentListener() {
+                       public void adjustmentValueChanged(AdjustmentEvent e)
+                       {
+                               selectPoint(e.getValue());
+                       }
+               });
+               _scroller.setEnabled(false);
+
+               // Add panel for waypoints / photos
+               JPanel listsPanel = new JPanel();
+               listsPanel.setLayout(new BoxLayout(listsPanel, BoxLayout.Y_AXIS));
+               listsPanel.setBorder(BorderFactory.createCompoundBorder(
+                       BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
+               );
+               _waypointListModel = new WaypointListModel(_trackInfo.getTrack());
+               _waypointList = new JList(_waypointListModel);
+               _waypointList.setVisibleRowCount(NUM_LIST_ENTRIES);
+               _waypointList.addListSelectionListener(new ListSelectionListener() {
+                       public void valueChanged(ListSelectionEvent e)
+                       {
+                               if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex());
+                       }});
+               listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints")));
+               listsPanel.add(new JScrollPane(_waypointList));
+               _photoListModel = new PhotoListModel(_trackInfo.getPhotoList());
+               _photoList = new JList(_photoListModel);
+               _photoList.setVisibleRowCount(NUM_LIST_ENTRIES);
+               _photoList.addListSelectionListener(new ListSelectionListener() {
+                       public void valueChanged(ListSelectionEvent e)
+                       {
+                               if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex());
+                       }});
+               listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos")));
+               listsPanel.add(new JScrollPane(_photoList));
+               listsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+               // add the controls to the main panel
+               mainPanel.add(trackDetailsPanel);
+               mainPanel.add(Box.createVerticalStrut(5));
+               mainPanel.add(_scroller);
+               mainPanel.add(Box.createVerticalStrut(5));
+               mainPanel.add(listsPanel);
+
+               // add the main panel at the top
+               add(mainPanel, BorderLayout.NORTH);
+               // set preferred width to be small
+               setPreferredSize(new Dimension(100, 100));
+       }
+
+
+       /**
+        * Select the specified point
+        * @param inValue value to select
+        */
+       private void selectPoint(int inValue)
+       {
+               if (_track != null && !_ignoreScrollEvents)
+               {
+                       _trackInfo.getSelection().selectPoint(inValue);
+               }
+       }
+
+
+       /**
+        * Select the specified photo
+        * @param inPhotoIndex index of selected photo
+        */
+       private void selectPhoto(int inPhotoIndex)
+       {
+               _trackInfo.selectPhoto(inPhotoIndex);
+       }
+
+
+       /**
+        * Select the specified waypoint
+        * @param inWaypointIndex index of selected waypoint
+        */
+       private void selectWaypoint(int inWaypointIndex)
+       {
+               if (inWaypointIndex >= 0)
+               {
+                       _trackInfo.selectPoint(_waypointListModel.getWaypoint(inWaypointIndex));
+               }
+       }
+
+
+       /**
+        * Notification that Track has been updated
+        */
+       public void dataUpdated(byte inUpdateType)
+       {
+               // Update track data
+               if (_track == null || _track.getNumPoints() <= 0)
+               {
+                       _trackpointsLabel.setText(I18nManager.getText("details.notrack"));
+                       _filenameLabel.setText("");
+               }
+               else
+               {
+                       _trackpointsLabel.setText(I18nManager.getText("details.track.points") + ": "
+                               + _track.getNumPoints());
+                       int numFiles = _trackInfo.getFileInfo().getNumFiles();
+                       if (numFiles == 1)
+                       {
+                               _filenameLabel.setText(I18nManager.getText("details.track.file") + ": "
+                                       + _trackInfo.getFileInfo().getFilename());
+                       }
+                       else if (numFiles > 1)
+                       {
+                               _filenameLabel.setText(I18nManager.getText("details.track.numfiles") + ": "
+                                       + numFiles);
+                       }
+                       else _filenameLabel.setText("");
+               }
+
+               // Update scroller settings
+               int currentPointIndex = _trackInfo.getSelection().getCurrentPointIndex();
+               _ignoreScrollEvents = true;
+               if (_track == null || _track.getNumPoints() < 2)
+               {
+                       // careful to avoid event loops here
+                       // _scroller.setValue(0);
+                       _scroller.setEnabled(false);
+               }
+               else
+               {
+                       _scroller.setMaximum(_track.getNumPoints() + SCROLLBAR_INTERVAL);
+                       if (currentPointIndex >= 0)
+                               _scroller.setValue(currentPointIndex);
+                       _scroller.setEnabled(true);
+               }
+               _ignoreScrollEvents = false;
+
+               // update waypoints and photos if necessary
+               if ((inUpdateType |
+                       (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.WAYPOINTS_MODIFIED)) > 0)
+               {
+                       _waypointListModel.fireChanged();
+               }
+               if ((inUpdateType |
+                       (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.PHOTOS_MODIFIED)) > 0)
+               {
+                       _photoListModel.fireChanged();
+               }
+               // Deselect selected waypoint if selected point has since changed
+               if (_waypointList.getSelectedIndex() >= 0)
+               {
+                       if (_trackInfo.getCurrentPoint() == null
+                        || !_waypointListModel.getWaypoint(_waypointList.getSelectedIndex()).equals(_trackInfo.getCurrentPoint()))
+                       {
+                               // point is selected in list but different from current point - deselect
+                               _waypointList.clearSelection();
+                       }
+               }
+               // Do the same for the photos
+               if (_photoList.getSelectedIndex() >= 0)
+               {
+                       DataPoint trackPoint = _trackInfo.getCurrentPoint();
+                       Photo selectedPhoto = _photoListModel.getPhoto(_photoList.getSelectedIndex());
+                       // Get selected Photo, if it's still there
+                       DataPoint photoPoint = null;
+                       if (selectedPhoto != null) {
+                               photoPoint = _photoListModel.getPhoto(_photoList.getSelectedIndex()).getDataPoint();
+                       }
+                       // Compare selected photo with selected point
+                       if ( (photoPoint != null && (trackPoint == null || !photoPoint.equals(trackPoint)))
+                               || (_trackInfo.getSelection().getCurrentPhotoIndex() < 0) )
+                       {
+                               // photo is selected in list but different from current point - deselect
+                               _photoList.clearSelection();
+                               _trackInfo.getSelection().deselectPhoto();
+                       }
+               }
+       }
+}
diff --git a/tim/prune/gui/images/add_photo_icon.png b/tim/prune/gui/images/add_photo_icon.png
new file mode 100644 (file)
index 0000000..a9d70b8
Binary files /dev/null and b/tim/prune/gui/images/add_photo_icon.png differ
diff --git a/tim/prune/gui/images/add_textfile_icon.png b/tim/prune/gui/images/add_textfile_icon.png
new file mode 100644 (file)
index 0000000..356239b
Binary files /dev/null and b/tim/prune/gui/images/add_textfile_icon.png differ
diff --git a/tim/prune/gui/images/connect_photo_icon.png b/tim/prune/gui/images/connect_photo_icon.png
new file mode 100644 (file)
index 0000000..d32d52e
Binary files /dev/null and b/tim/prune/gui/images/connect_photo_icon.png differ
diff --git a/tim/prune/gui/images/edit_point_icon.gif b/tim/prune/gui/images/edit_point_icon.gif
new file mode 100644 (file)
index 0000000..1dc19a3
Binary files /dev/null and b/tim/prune/gui/images/edit_point_icon.gif differ
diff --git a/tim/prune/gui/images/save_icon.gif b/tim/prune/gui/images/save_icon.gif
new file mode 100644 (file)
index 0000000..499dd0c
Binary files /dev/null and b/tim/prune/gui/images/save_icon.gif differ
diff --git a/tim/prune/gui/images/set_end_icon.png b/tim/prune/gui/images/set_end_icon.png
new file mode 100644 (file)
index 0000000..6134958
Binary files /dev/null and b/tim/prune/gui/images/set_end_icon.png differ
diff --git a/tim/prune/gui/images/set_start_icon.png b/tim/prune/gui/images/set_start_icon.png
new file mode 100644 (file)
index 0000000..a3196ce
Binary files /dev/null and b/tim/prune/gui/images/set_start_icon.png differ
diff --git a/tim/prune/gui/images/undo_icon.gif b/tim/prune/gui/images/undo_icon.gif
new file mode 100644 (file)
index 0000000..eae118a
Binary files /dev/null and b/tim/prune/gui/images/undo_icon.gif differ
index ff556566ea9ce4e4e9592af815d47eefa3506149..5214018858bb264fa3e03487c42b90678ed8beb7 100644 (file)
@@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Each to nearest track point
 menu.select=Select
 menu.select.all=Select all
 menu.select.none=Select none
+menu.select.start=Set range start
+menu.select.end=Set range end
+menu.photo=Photo
+menu.photo.saveexif=Save to Exif
+menu.photo.connect=Connect to point
+menu.photo.delete=Remove photo
 menu.3d=Three-D
 menu.3d.show3d=Show in Three-D
 menu.help=Help
@@ -42,6 +48,10 @@ dialog.exit.confirm.title=Exit Prune
 dialog.exit.confirm.text=Your data is not saved. Are you sure you want to exit?
 dialog.openappend.title=Append to existing data
 dialog.openappend.text=Append this data to the data already loaded?
+dialog.deletepoint.title=Delete Point
+dialog.deletepoint.deletephoto=Delete photo attached to this point?
+dialog.deletephoto.title=Delete Photo
+dialog.deletephoto.deletepoint=Delete point attached to this photo?
 dialog.deleteduplicates.title=Delete Duplicates
 dialog.deleteduplicates.single.text=duplicate was deleted
 dialog.deleteduplicates.multi.text=duplicates were deleted
@@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=No records
 dialog.openoptions.tabledesc=Extract of file
 dialog.openoptions.altitudeunits=Altitude units
 dialog.jpegload.subdirectories=Include subdirectories
+dialog.jpegload.loadjpegswithoutcoords=Include photos without coordinates
 dialog.jpegload.progress.title=Loading photos
 dialog.jpegload.progress=Please wait while the photos are searched
 dialog.jpegload.title=Loaded photos
@@ -89,8 +100,10 @@ dialog.save.ok2=points to file
 dialog.save.overwrite.title=File already exists
 dialog.save.overwrite.text=This file already exists. Are you sure you want to overwrite the file?
 dialog.exportkml.title=Export KML
-dialog.exportkml.text=Please enter a short description for the data
-dialog.exportkml.filetype=KML files
+dialog.exportkml.text=Title for the data
+dialog.exportkml.kmz=Compress to make kmz file
+dialog.exportkml.exportimages=Export image thumbnails to kmz
+dialog.exportkml.filetype=KML, KMZ files
 dialog.exportpov.title=Export POV
 dialog.exportpov.text=Please enter the parameters for the POV export
 dialog.exportpov.font=Font
@@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint name
 dialog.pointnameedit.uppercase=UPPER case
 dialog.pointnameedit.lowercase=lower case
 dialog.pointnameedit.sentencecase=Sentence case
+dialog.saveexif.title=Save Exif
+dialog.saveexif.intro=Select the photos to save using the checkboxes
+dialog.saveexif.nothingtosave=Coordinate data is unchanged, nothing to save
+dialog.saveexif.noexiftool=No exiftool program could be found. Continue?
+dialog.saveexif.table.photoname=Photo name
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Save
+dialog.saveexif.photostatus.connected=Connected
+dialog.saveexif.photostatus.disconnected=Disconnected
+dialog.saveexif.photostatus.modified=Modified
+dialog.saveexif.overwrite=Overwrite files
+dialog.saveexif.ok1=Saved
+dialog.saveexif.ok2=photo files
 dialog.about.title=About Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -131,10 +157,29 @@ dialog.about.summarytext1=Prune is a program for loading, displaying and editing
 dialog.about.summarytext2=It is released under the Gnu GPL for free, open, worldwide use and enhancement.<br>Copying, redistribution and modification are permitted and encouraged<br>according to the conditions in the included <code>license.txt</code> file.
 dialog.about.summarytext3=Please see <code style="font-weight:bold">http://activityworkshop.net/</code> for more information and user guides.
 dialog.about.translatedby=English text by activityworkshop.
+dialog.about.systeminfo=System info
+dialog.about.systeminfo.os=Operating System
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d installed
+dialog.about.systeminfo.povray=Povray installed
+dialog.about.systeminfo.exiftool=Exiftool installed
+dialog.about.yes=Yes
+dialog.about.no=No
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune code written by
+dialog.about.credits.exifcode=Exif code by
+dialog.about.credits.icons=Some icons taken from
+dialog.about.credits.translations=Translations helped by
+dialog.about.credits.devtools=Development tools
+dialog.about.credits.othertools=Other tools
+dialog.about.credits.thanks=Thanks to
 
 # 3d window
 dialog.3d.title=Prune Three-d view
 dialog.3d.altitudecap=Minimum altitude range
+dialog.3dlines.title=Prune gridlines
+dialog.3dlines.empty=No gridlines to display!
+dialog.3dlines.intro=These are the gridlines for the three-d view
 
 # Buttons
 button.ok=OK
@@ -145,14 +190,19 @@ button.cancel=Cancel
 button.overwrite=Overwrite
 button.moveup=Move up
 button.movedown=Move down
-button.startrange=Set start
-button.endrange=Set end
 button.deletepoint=Delete point
 button.deleterange=Delete range
+button.showlines=Show lines
 button.edit=Edit
 button.exit=Exit
 button.close=Close
 button.continue=Continue
+button.yes=Yes
+button.no=No
+button.yestoall=Yes to all
+button.notoall=No to all
+button.selectall=Select all
+button.selectnone=Select none
 
 # Display components
 display.nodata=No data loaded
@@ -181,6 +231,9 @@ display.range.time.hours=h
 display.range.time.days=d
 details.waypointsphotos.waypoints=Waypoints
 details.waypointsphotos.photos=Photos
+details.photodetails=Photo details
+details.nophoto=No photo selected
+details.photo.loading=Loading
 
 # Field names
 fieldname.latitude=Latitude
@@ -219,19 +272,27 @@ undo.load=load data
 undo.loadphotos=load photos
 undo.editpoint=edit point
 undo.deletepoint=delete point
+undo.deletephoto=remove photo
 undo.deleterange=delete range
 undo.compress=compress track
 undo.insert=insert points
 undo.deleteduplicates=delete duplicates
 undo.reverse=reverse range
 undo.rearrangewaypoints=rearrange waypoints
+undo.connectphoto=connect photo
 
 # Error messages
 error.save.dialogtitle=Error saving data
 error.save.nodata=No data to save
 error.save.failed=Failed to save the data to file:
+error.saveexif.filenotfound=Failed to find photo file
+error.saveexif.cannotoverwrite1=Photo file
+error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy?
 error.load.dialogtitle=Error loading data
 error.load.noread=Cannot read file
+error.load.nopoints=No coordinate information found in the file
+error.load.unknownxml=Unrecognised xml format:
+error.load.othererror=Error reading file:
 error.jpegload.dialogtitle=Error loading photos
 error.jpegload.nofilesfound=No files found
 error.jpegload.nojpegsfound=No jpeg files found
index 98fc57c6b94199e98bfe0c5de649a64821bb20a9..3a53f655ef355e47f629ae9b556342b17575eb2b 100644 (file)
@@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Jeder zum n
 menu.select=Selektieren
 menu.select.all=Alles selektieren
 menu.select.none=Nichts selektieren
+menu.select.start=Start setzen
+menu.select.end=Stopp setzen
+menu.photo=Foto
+menu.photo.saveexif=Exif Daten speichern
+menu.photo.connect=Mit Punkt verbinden
+menu.photo.delete=Foto entfernen
 menu.3d=Drei-D
 menu.3d.show3d=In drei-D zeigen
 menu.help=Hilfe
@@ -42,6 +48,10 @@ dialog.exit.confirm.title=Prune beenden
 dialog.exit.confirm.text=Ihre Daten wurden nicht gespeichert. Wollen Sie trotzdem das Programm beenden?
 dialog.openappend.title=Daten anhängen oder ersetzen
 dialog.openappend.text=Häng diese Daten zu den aktuellen Daten an?
+dialog.deletepoint.title=Punkt löschen
+dialog.deletepoint.deletephoto=Foto von diesem Punkt auch löschen?
+dialog.deletephoto.title=Photo entfernen
+dialog.deletephoto.deletepoint=Punkt von diesem Foto auch löschen?
 dialog.deleteduplicates.title=Duplikate löschen
 dialog.deleteduplicates.single.text=Duplikat wurde gelöscht
 dialog.deleteduplicates.multi.text=Duplikate wurden gelöscht
@@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=Keine Rekords
 dialog.openoptions.tabledesc=Extrakt von der Datei
 dialog.openoptions.altitudeunits=Höhe Maßeinheiten
 dialog.jpegload.subdirectories=Subordnern auch durchsuchen
+dialog.jpegload.loadjpegswithoutcoords=Auch Fotos ohne Koordinaten laden
 dialog.jpegload.progress.title=Fotos werden geladen
 dialog.jpegload.progress=Bitte warten während die Fotos durchgesucht werden
 dialog.jpegload.title=Fotos geladen
@@ -89,8 +100,10 @@ dialog.save.ok2=Punkte gespeichert nach
 dialog.save.overwrite.title=Datei existiert
 dialog.save.overwrite.text=Diese Datei existiert schon. Sind Sie sicher, Sie wollen die Datei Ã¼berschreiben?
 dialog.exportkml.title=KML exportieren
-dialog.exportkml.text=Kurze Beschreibung von den Daten
-dialog.exportkml.filetype=KML Dateien
+dialog.exportkml.text=Titel für die Daten
+dialog.exportkml.kmz=Daten ins kmz Datei komprimieren
+dialog.exportkml.exportimages=Bilder ins kmz exportieren
+dialog.exportkml.filetype=KML, KMZ Dateien
 dialog.exportpov.title=POV exportieren
 dialog.exportpov.text=Geben Sie die Parameter ein für das POV Export
 dialog.exportpov.font=Font
@@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint Name
 dialog.pointnameedit.uppercase=GROSS geschrieben
 dialog.pointnameedit.lowercase=klein geschrieben
 dialog.pointnameedit.sentencecase=Gemischt geschrieben
+dialog.saveexif.title=Exif speichern
+dialog.saveexif.intro=Selektieren Sie die Fotos zu speichern
+dialog.saveexif.nothingtosave=Koordinaten sind nicht modifiziert, nichts zu speichern
+dialog.saveexif.noexiftool=Kein exiftool Program gefunden. Trotzdem fortfahren?
+dialog.saveexif.table.photoname=Foto Name
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Speichern
+dialog.saveexif.photostatus.connected=Verbunden
+dialog.saveexif.photostatus.disconnected=Getrennt
+dialog.saveexif.photostatus.modified=Modifiziert
+dialog.saveexif.overwrite=Dateien Ã¼berschreiben
+dialog.saveexif.ok1=Es wurden
+dialog.saveexif.ok2=Foto Dateien geschrieben
 dialog.about.title=Ãœber Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -131,10 +157,29 @@ dialog.about.summarytext1=Prune ist ein Programm f
 dialog.about.summarytext2=Es ist unter den Gnu GPL zur Verfügung gestellt, für frei, gratis und offen Gebrauch und Weiterentwicklung.<br>Kopieren, Weiterverbreitung und Veränderungen sind erlaubt und willkommen<br>unter die Bedingungen in der enthaltenen <code>license.txt</code> Datei.
 dialog.about.summarytext3=Bitte sehen Sie <code style="font-weight:bold">http://activityworkshop.net/</code> für weitere Information und Benutzeranleitungen.
 dialog.about.translatedby=Deutsche Ãœbersetzung von activityworkshop.
+dialog.about.systeminfo=System Information
+dialog.about.systeminfo.os=Betriebsystem
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d installiert
+dialog.about.systeminfo.povray=Povray installiert
+dialog.about.systeminfo.exiftool=Exiftool installiert
+dialog.about.yes=Ja
+dialog.about.no=Nein
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune Code geschrieben von
+dialog.about.credits.exifcode=Exif Code von
+dialog.about.credits.icons=Einige Ikons von
+dialog.about.credits.translations=Ãœbersetzungen mit Hilfe von
+dialog.about.credits.devtools=Entwicklungsprogrammen
+dialog.about.credits.othertools=Andere Programmen
+dialog.about.credits.thanks=Danke an
 
 # 3d window
 dialog.3d.title=Prune Drei-D Ansicht
 dialog.3d.altitudecap=Minimum Höhenskala
+dialog.3dlines.title=Prune Gitterlinien
+dialog.3dlines.empty=Keine Linien zum anzeigen!
+dialog.3dlines.intro=Hier sind die Linien für die drei-D Ansicht
 
 # Buttons
 button.ok=OK
@@ -145,14 +190,19 @@ button.cancel=Abbrechen
 button.overwrite=Ãœberschreiben
 button.moveup=Aufwärts moven
 button.movedown=Abwärts moven
-button.startrange=Start setzen
-button.endrange=Stopp setzen
 button.deletepoint=Punkt löschen
 button.deleterange=Spanne löschen
+button.showlines=Linien anzeigen
 button.edit=Bearbeiten
 button.exit=Beenden
 button.close=Schließen
 button.continue=Fortsetzen
+button.yes=Ja
+button.no=Nein
+button.yestoall=Ja für alle
+button.notoall=Nein für alle
+button.selectall=Alle selektieren
+button.selectnone=Nichts selektieren
 
 # Display components
 display.nodata=Keine Daten geladen
@@ -181,6 +231,9 @@ display.range.time.hours=Std
 display.range.time.days=T
 details.waypointsphotos.waypoints=Waypoints
 details.waypointsphotos.photos=Fotos
+details.photodetails=Details vom Foto
+details.nophoto=Kein Foto selektiert
+details.photo.loading=Laden
 
 # Field names
 fieldname.latitude=Breitengrad
@@ -219,19 +272,27 @@ undo.load=Daten laden
 undo.loadphotos=Fotos laden
 undo.editpoint=Punkt bearbeiten
 undo.deletepoint=Punkt löschen
+undo.deletephoto=Photo entfernen
 undo.deleterange=Spanne löschen
 undo.compress=Track komprimieren
 undo.insert=Punkte hinzufügen
 undo.deleteduplicates=Duplikaten löschen
 undo.reverse=Spanne umdrehen
 undo.rearrangewaypoints=Waypoints reorganisieren
+undo.connectphoto=Foto verbinden
 
 # Error messages
 error.save.dialogtitle=Fehler beim Speichern
 error.save.nodata=Keine Daten wurden geladen
-error.save.failed=Speichern von der Datei fehlgeschlagen :
+error.save.failed=Speichern von der Datei fehlgeschlagen:
+error.saveexif.filenotfound=Foto Datei nicht gefunden
+error.saveexif.cannotoverwrite1=Foto Datei
+error.saveexif.cannotoverwrite2=ist schreib-geschützt. Speichern zu einer Kopie?
 error.load.dialogtitle=Fehler beim Laden
 error.load.noread=Datei konnte nicht gelesen werden
+error.load.nopoints=Keine gültigen Daten in Datei gefunden
+error.load.unknownxml=Unbekanntes xml Format:
+error.load.othererror=Fehler beim Lesen von der Datei:
 error.jpegload.dialogtitle=Fehler beim Laden von Fotos
 error.jpegload.nofilesfound=Keine Dateien gefunden
 error.jpegload.nojpegsfound=Keine Jpeg Dateien gefunden
index e48b3b70c3c01a711b9f9caf5d955c1d60fe26e5..7b2013eab37282eb2176f37f252eb7e4827d5501 100644 (file)
@@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Jede zum n
 menu.select=Selektiere
 menu.select.all=Alles selektiere
 menu.select.none=Nüüt selektiere
+menu.select.start=Start setzä
+menu.select.end=Stopp setzä
+menu.photo=Föteli
+menu.photo.saveexif=Exif Date speicherä
+menu.photo.connect=Mitem Punkt verbindä
+menu.photo.delete=Föteli entfernä
 menu.3d=Drüü-D
 menu.3d.show3d=In drüü-D zeigä
 menu.help=Hilfe
@@ -42,6 +48,10 @@ dialog.exit.confirm.title=Prune be
 dialog.exit.confirm.text=Ihri Date sind nonig gspeicheret worde. Wend Sie trotzdem s Programm beände?
 dialog.openappend.title=Date aahänge oder ersätze
 dialog.openappend.text=Häng diese Date zur aktuelli Daten aa?
+dialog.deletepoint.title=Punkt löschä
+dialog.deletepoint.deletephoto=s Föteli vonem Punkt au löschä?
+dialog.deletephoto.title=Föteli entfernä
+dialog.deletephoto.deletepoint=Punkt vonem Föteli au löschä?
 dialog.deleteduplicates.title=Duplikaten lösche
 dialog.deleteduplicates.single.text=Duplikat isch glöscht worde
 dialog.deleteduplicates.multi.text=Duplikaten sin glöscht worde
@@ -69,28 +79,31 @@ dialog.openoptions.deliminfo.norecords=Kei Rekords
 dialog.openoptions.tabledesc=Extrakt vom File
 dialog.openoptions.altitudeunits=Höchi Masseiheite
 dialog.jpegload.subdirectories=Subordnern au
+dialog.jpegload.loadjpegswithoutcoords=Au Fötelis ohni Koordinate
 dialog.jpegload.progress.title=Fötelis lade
-dialog.jpegload.progress=Bitte warte während die Fötolis durägsucht werde
+dialog.jpegload.progress=Bitte warte während die Fötelis durägsucht werde
 dialog.jpegload.title=Fötelis glade worde
 dialog.jpegload.photoadded=Föteli isch glade worde
 dialog.jpegload.photosadded=Fötelis sin glade worde
-dialog.saveoptions.title=File speichere
-dialog.save.fieldstosave=Fälder zu speichere
+dialog.saveoptions.title=File speicherä
+dialog.save.fieldstosave=Fälder zu speicherä
 dialog.save.table.field=Fäld
 dialog.save.table.hasdata=Het Date
-dialog.save.table.save=Speichere
-dialog.save.headerrow=Titel Ziile speichere
+dialog.save.table.save=Speicherä
+dialog.save.headerrow=Titel Ziile speicherä
 dialog.save.coordinateunits=Koordinate Massiiheite
 dialog.save.units.original=Original
 dialog.save.altitudeunits=Höchi Massiiheite
 dialog.save.oktitle=File gespeichert worde
-dialog.save.ok1=Es isch
-dialog.save.ok2=Punkte gespeichert worde na
+dialog.save.ok1=Es sin
+dialog.save.ok2=Punkte gspeicheret worde na
 dialog.save.overwrite.title=s'File existiert scho
 dialog.save.overwrite.text=s'File existiert scho. Sind Sie sicher, Sie wend s'File Ã¼berschriibe?
-dialog.exportkml.title=KML exportiere
-dialog.exportkml.text=Kurze Beschriibig von den Date
-dialog.exportkml.filetype=KML Dateie
+dialog.exportkml.title=KML exportierä
+dialog.exportkml.text=Titel für die Date
+dialog.exportkml.kmz=Date ins kmz File komprimierä
+dialog.exportkml.exportimages=Bildli ins Kmz exportierä
+dialog.exportkml.filetype=KML, KMZ Dateie
 dialog.exportpov.title=POV exportiere
 dialog.exportpov.text=Gäbet Sie die Parameter ii fürs POV Export
 dialog.exportpov.font=Font
@@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint Name
 dialog.pointnameedit.uppercase=GROSS gschriebe
 dialog.pointnameedit.lowercase=chli gschriebe
 dialog.pointnameedit.sentencecase=Gmischt gschriebe
+dialog.saveexif.title=Exif go speicherä
+dialog.saveexif.intro=Wählet Sie die Fötelis uus zum speicherä
+dialog.saveexif.nothingtosave=Koordinaten sin nöd geänderet, nüüt zum speicherä
+dialog.saveexif.noexiftool=Kei exiftool Programm gfunde. Wiiter?
+dialog.saveexif.table.photoname=Föteli Name
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Speicherä
+dialog.saveexif.photostatus.connected=Verbundä
+dialog.saveexif.photostatus.disconnected=Gtrännt
+dialog.saveexif.photostatus.modified=Gänderet
+dialog.saveexif.overwrite=Files Ã¼berschriebä
+dialog.saveexif.ok1=Es sin
+dialog.saveexif.ok2=Fötelis gschriebe worde
 dialog.about.title=Ãœber Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -131,28 +157,52 @@ dialog.about.summarytext1=Prune isch n Programm f
 dialog.about.summarytext2=Es isch unter den Gnu GPL zur Verfüegig gstellt,für frei, gratis und offen Gebruuch und Wiiterentwicklig.<br>Kopiere, Wiiterverbreitig und Veränderige sin erlaubt und willkommen<br>unter die Bedingunge im enthaltene <code>license.txt</code> File.
 dialog.about.summarytext3=Bitte lueg na <code style="font-weight:bold">http://activityworkshop.net/</code> für wiitere Information und Benutzeraaleitige.
 dialog.about.translatedby=Schwiizerdüütschi Ãœbersetzig vo activityworkshop.
+dialog.about.systeminfo=Syschtem Info
+dialog.about.systeminfo.os=Betriebsyschtem
+dialog.about.systeminfo.java=Version vonem Java
+dialog.about.systeminfo.java3d=Java3d inschtalliert
+dialog.about.systeminfo.povray=Povray inschtalliert
+dialog.about.systeminfo.exiftool=Exiftool inschtalliert
+dialog.about.yes=Ja
+dialog.about.no=Nei
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune Code gschriebä vo
+dialog.about.credits.exifcode=Exif Code vo
+dialog.about.credits.icons=Einigi Ikons vo
+dialog.about.credits.translations=Ãœbersetzige mit dr Hilfe vo
+dialog.about.credits.devtools=Entwicklungswärkzüüge
+dialog.about.credits.othertools=Anderi Wärkzüüge
+dialog.about.credits.thanks=Danke an
 
 # 3d window
-dialog.3d.title=Prune Drüü-d aasicht
+dialog.3d.title=Prune Drüü-d Aasicht
 dialog.3d.altitudecap=Minimum Höhenskala
+dialog.3dlines.title=Prune Gitterlinie
+dialog.3dlines.empty=Kei Linie zum aazeigä!
+dialog.3dlines.intro=Hier sin die Linie für die drüü-D Aasicht
 
 # Buttons
 button.ok=OK
 button.back=Zrugg
-button.next=Nöchste
+button.next=Nöchstä
 button.finish=Fertig
-button.cancel=Abbräche
-button.overwrite=Ãœberschriibe
-button.moveup=Uufwärts move
-button.movedown=Abwärts move
-button.startrange=Start setze
-button.endrange=Stopp setze
-button.deletepoint=Punkt lösche
-button.deleterange=Spanne lösche
-button.edit=Editiere
-button.exit=Beände
-button.close=Schliesse
-button.continue=Fortsetze
+button.cancel=Abbrächä
+button.overwrite=Überschriibä
+button.moveup=Uufä schiebä
+button.movedown=Aba schiebä
+button.deletepoint=Punkt löschä
+button.deleterange=Spanne löschä
+button.showlines=Linie aazeigä
+button.edit=Editierä
+button.exit=Beändä
+button.close=Schliessä
+button.continue=Fortsetzä
+button.yes=Ja
+button.no=Nei
+button.yestoall=Ja für alli
+button.notoall=Nei für alli
+button.selectall=Alli selektierä
+button.selectnone=Nüüt selektierä
 
 # Display components
 display.nodata=Kei Date glade worde
@@ -181,6 +231,9 @@ display.range.time.hours=Std
 display.range.time.days=T
 details.waypointsphotos.waypoints=Waypoints
 details.waypointsphotos.photos=Fötelis
+details.photodetails=Details vom Föteli
+details.nophoto=Kei föteli selektiert
+details.photo.loading=Ladä
 
 # Field names
 fieldname.latitude=Breitegrad
@@ -215,23 +268,31 @@ cardinal.e=O
 cardinal.w=W
 
 # Undo operations
-undo.load=Date lade
-undo.loadphotos=Fötelis lade
-undo.editpoint=Punkt editiere
-undo.deletepoint=Punkt lösche
-undo.deleterange=Spanne lösche
-undo.compress=Track komprimiere
-undo.insert=Punkte innätue
-undo.deleteduplicates=Duplikaten lösche
+undo.load=Date ladä
+undo.loadphotos=Fötelis ladä
+undo.editpoint=Punkt editierä
+undo.deletepoint=Punkt löschä
+undo.deletephoto=Föteli entfärnä
+undo.deleterange=Spanne löschä
+undo.compress=Track komprimierä
+undo.insert=Punkte innätuä
+undo.deleteduplicates=Duplikaten löschä
 undo.reverse=Spanne umdrähie
-undo.rearrangewaypoints=Waypoints reorganisiere
+undo.rearrangewaypoints=Waypoints reorganisierä
+undo.connectphoto=Föteli verbindä
 
 # Error messages
 error.save.dialogtitle=Fehler bim Speichere
 error.save.nodata=Kei Date zum speichere
 error.save.failed=Speichere vom File fehlgschlage :
+error.saveexif.filenotfound=Föteli File nöd gfunde
+error.saveexif.cannotoverwrite1=Föteli File
+error.saveexif.cannotoverwrite2=isch nöd schriibbar. Speichere na einer Kopie?
 error.load.dialogtitle=Fehler bim Lade
 error.load.noread=File cha nöd glase werde
+error.load.nopoints=Kei gültigi Information im Datei gfunde
+error.load.unknownxml=Unbekanntes xml Format:
+error.load.othererror=Fehler bim Läse:
 error.jpegload.dialogtitle=Fehler bim Lade von Fötelis
 error.jpegload.nofilesfound=Kei Dateie gfunde
 error.jpegload.nojpegsfound=Kei Jpegs gfunde
index 3d797ad7fa005e6af951a7c911f529391cf0a5b8..fcd83cf5a7c58295b0a37188556f20e90ad13baa 100644 (file)
@@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Ir al m
 menu.select=Seleccionar
 menu.select.all=Seleccionar todo
 menu.select.none=Seleccionar nada
+menu.select.start=Fijar comienzo
+menu.select.end=Fijar final
+menu.photo=Foto
+menu.photo.saveexif=Guardar Exif
+menu.photo.connect=Connect con punto
+menu.photo.delete=Eliminar foto
 menu.3d=3-D
 menu.3d.show3d=Mostrar en 3-D
 menu.help=Ayuda
@@ -42,6 +48,10 @@ dialog.exit.confirm.title=Salir de Prune
 dialog.exit.confirm.text=Los datos han sido modificados. Desea salir de Prune?
 dialog.openappend.title=Agregar a datos existentes
 dialog.openappend.text=Agregar estos datos a los datos ya guardados?
+dialog.deletepoint.title=Borrar punto
+dialog.deletepoint.deletephoto=Borrar foto tambien?
+dialog.deletephoto.title=Borrar foto
+dialog.deletephoto.deletepoint=Borrar punto tambien?
 dialog.deleteduplicates.title=Borrar duplicados
 dialog.deleteduplicates.single.text=duplicado eliminado
 dialog.deleteduplicates.multi.text=duplicados eliminados
@@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=Ningun dato
 dialog.openoptions.tabledesc=Extraer archivo
 dialog.openoptions.altitudeunits=Unidades altitud
 dialog.jpegload.subdirectories=Incluir subdirectorios
+dialog.jpegload.loadjpegswithoutcoords=Fotos sin coordenadas tambien
 dialog.jpegload.progress.title=Cargando fotos
 dialog.jpegload.progress=Por favor espere mientras se buscan las fotos
 dialog.jpegload.title=Fotos cargadas
@@ -89,8 +100,10 @@ dialog.save.ok2=puntos al archivo
 dialog.save.overwrite.title=El archivo ya existe
 dialog.save.overwrite.text=El archivo ya existe, desea sobreescribirlo?
 dialog.exportkml.title=Exportar KML
-dialog.exportkml.text=Introduzca breve descripción para los datos
-dialog.exportkml.filetype=Archivos KML
+dialog.exportkml.text=Descripción para los datos
+dialog.exportkml.kmz=Comprimir al archivo kmz
+dialog.exportkml.exportimages=Exportar fotos al kmz
+dialog.exportkml.filetype=Archivos KML, KMZ
 dialog.exportpov.title=Exportar POV
 dialog.exportpov.text=Introdzca los Parametros para exportar
 dialog.exportpov.font=Fuente
@@ -98,7 +111,7 @@ dialog.exportpov.camerax=Camera X
 dialog.exportpov.cameray=Camera Y
 dialog.exportpov.cameraz=Camera Z
 dialog.exportpov.filetype=Archivos POV
-dialog.exportpov.warningtracksize=This track has a large number of points, which Java3D might not be able to display.\nAre you sure you want to continue?
+dialog.exportpov.warningtracksize=Este track contiene un gran numero de puntos. Puede ser que Java3D no los pueda visualizar. Está seguro de que desea continuar?
 dialog.confirmreversetrack.title=Confirmar inversión
 dialog.confirmreversetrack.text=Este track contiene información sobre la fecha, que estará fuera de secuencia después de la inversión. Esta seguro que desea invertir esta sección?
 dialog.interpolate.title=Interpolar puntos
@@ -124,17 +137,49 @@ dialog.pointnameedit.name=Nombre de waypoint
 dialog.pointnameedit.uppercase=Maysculas
 dialog.pointnameedit.lowercase=minsculas
 dialog.pointnameedit.sentencecase=Mezcla
+dialog.saveexif.title=Guardar Exif
+dialog.saveexif.intro=Seleccione fotos a guardar
+dialog.saveexif.nothingtosave=Coordenadas no han modificados
+dialog.saveexif.noexiftool=exiftool program no encontrado. Desea continuar?
+dialog.saveexif.table.photoname=Nombre de foto
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Guardar
+dialog.saveexif.photostatus.connected=Connected
+dialog.saveexif.photostatus.disconnected=Disconnected
+dialog.saveexif.photostatus.modified=Modificado
+dialog.saveexif.overwrite=Sobreescribirlar archivos?
+dialog.saveexif.ok1=Guardando
+dialog.saveexif.ok2=fotos
 dialog.about.title=Acerca de Prune
 dialog.about.version=Versión
 dialog.about.build=Construir
 dialog.about.summarytext1=Prune es un programa para cargar, mostrar y editar datos de receptores GPS.
 dialog.about.summarytext2=Distribuido bajo el GNU GPL para uso libre y gratuito.<br>Se permite (y se anima) la copia, redistribución y modificación de acuerdo<br>a las condiciones incluidas en el archivo <code>licence.txt</code>.
 dialog.about.summarytext3=Por favor, ver <code style="font-weight:bold">http://activityworkshop.net/</code> para más información y guías del usuario.
-dialog.about.translatedby=Traducción en español por activityworkshop y un alma muy gentil!
+dialog.about.translatedby=Traducción al español realizada por activityworkshop y amigos muy amables!
+dialog.about.systeminfo=Informacion del System
+dialog.about.systeminfo.os=Operating System
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d installed
+dialog.about.systeminfo.povray=Povray installed
+dialog.about.systeminfo.exiftool=Exiftool installed
+dialog.about.yes=Si
+dialog.about.no=No
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune code written by
+dialog.about.credits.exifcode=Exif code by
+dialog.about.credits.icons=Some icons taken from
+dialog.about.credits.translations=Translations helped by
+dialog.about.credits.devtools=Development tools
+dialog.about.credits.othertools=Other tools
+dialog.about.credits.thanks=Thanks to
 
 # 3d window
 dialog.3d.title=Prune vista 3-D
 dialog.3d.altitudecap=Escala de las altitudes
+dialog.3dlines.title=Prune gridlines
+dialog.3dlines.empty=Ninguna información coordenadas encontrada!
+dialog.3dlines.intro=Información de gridlines
 
 # Buttons
 button.ok=Aceptar
@@ -145,14 +190,19 @@ button.cancel=Cancelar
 button.overwrite=Sobreescribir
 button.moveup=Mover hacia arriba
 button.movedown=Mover hacia abajo
-button.startrange=Fijar comienzo
-button.endrange=Fijar final
 button.deletepoint=Eliminar punto
 button.deleterange=Eliminar rango
+button.showlines=Mostrar gridlines
 button.edit=Editar
 button.exit=Salir
 button.close=Cerrar
 button.continue=Continúe
+button.yes=Si
+button.no=No
+button.yestoall=Si por todo
+button.notoall=No por todo
+button.selectall=Seleccionar todo
+button.selectnone=Seleccionar nada
 
 # Display components
 display.nodata=Ningún dato cargado
@@ -181,6 +231,9 @@ display.range.time.hours=h
 display.range.time.days=d
 details.waypointsphotos.waypoints=Waypoints
 details.waypointsphotos.photos=Fotos
+details.photodetails=Detalles del Foto
+details.nophoto=Ningún foto seleccionado
+details.photo.loading=Cargar
 
 # Field names
 fieldname.latitude=Latitud
@@ -219,19 +272,27 @@ undo.load=cargar datos
 undo.loadphotos=cargar fotos
 undo.editpoint=editar punto
 undo.deletepoint=eliminar punto
+undo.deletephoto=eliminar foto
 undo.deleterange=eliminar rango
 undo.compress=comprimir track
 undo.insert=insertar puntos
 undo.deleteduplicates=eliminar duplicados
 undo.reverse=invertir rango
 undo.rearrangewaypoints=reordenar waypoints
+undo.connectphoto=connectar foto
 
 # Error messages
 error.save.dialogtitle=Fallo al guardar datos
 error.save.nodata=Ningún dato salvado
 error.save.failed=Fallo al guardar datos al archivo:
+error.saveexif.filenotfound=Archivo no encontrado
+error.saveexif.cannotoverwrite1=No se puede guardar
+error.saveexif.cannotoverwrite2=. Guardar a una copia?
 error.load.dialogtitle=Fallo al cargar datos
 error.load.noread=No se puede leer el fichero
+error.load.nopoints=Ninguna información coordenadas encontrada
+error.load.unknownxml=Unrecognised xml format:
+error.load.othererror=Fallo al cargar datos:
 error.jpegload.dialogtitle=Error cargando fotos
 error.jpegload.nofilesfound=Ningún archivo encontrado
 error.jpegload.nojpegsfound=Ningún archivo jpeg encontrado
diff --git a/tim/prune/lang/prune-texts_fr.properties b/tim/prune/lang/prune-texts_fr.properties
new file mode 100644 (file)
index 0000000..59e070b
--- /dev/null
@@ -0,0 +1,309 @@
+# Text entries for the Prune application
+# French entries as extra
+
+# Menu entries
+menu.file=Fichier
+menu.file.open=Ouvrir
+menu.file.addphotos=Ouvrir photos
+menu.file.save=Enregistrer
+menu.file.exportkml=Exporter au KML
+menu.file.exportpov=Exporter au POV
+menu.file.exit=Quitter
+menu.edit=Édition
+menu.edit.undo=Undo
+menu.edit.clearundo=Purger undo liste
+menu.edit.editpoint=Editer point
+menu.edit.editwaypointname=Editer nom du waypoint
+menu.edit.deletepoint=Supprimer du point
+menu.edit.deleterange=Supprimer de range
+menu.edit.deleteduplicates=Supprimer des duplicates
+menu.edit.compress=Compacter track
+menu.edit.interpolate=Interpolate
+menu.edit.reverse=Reverse range
+menu.edit.rearrange=Rearrange waypoints
+menu.edit.rearrange.start=Tous Ã  tête de fichier
+menu.edit.rearrange.end=Tous Ã  pied de fichier
+menu.edit.rearrange.nearest=Chaque Ã  prochain point
+menu.select=Sélectionner
+menu.select.all=Tous sélectionner
+menu.select.none=Rien sélectionner
+menu.select.start=Set range start
+menu.select.end=Set range end
+menu.photo=Photo
+menu.photo.saveexif=Enregistrer Ã  Exif
+menu.photo.connect=Connect to point
+menu.photo.delete=Remove photo
+menu.3d=Trois-D
+menu.3d.show3d=Montrer en Trois-D
+menu.help=Aide
+menu.help.about=À propos de Prune
+# Popup menu for map
+menu.map.zoomin=Zoom avant
+menu.map.zoomout=Zoom arrière
+menu.map.zoomfull=Zoom to full scale
+menu.map.autopan=Pan automatique
+
+# Dialogs
+dialog.exit.confirm.title=Terminer Prune
+dialog.exit.confirm.text=Le data a Ã©té modifié. Souhaitez-vous terminer Prune sans enregistrement?
+dialog.openappend.title=Append to existing data
+dialog.openappend.text=Append this data to the data already loaded?
+dialog.deletepoint.title=Delete Point
+dialog.deletepoint.deletephoto=Delete photo attached to this point?
+dialog.deletephoto.title=Delete Photo
+dialog.deletephoto.deletepoint=Delete point attached to this photo?
+dialog.deleteduplicates.title=Delete Duplicates
+dialog.deleteduplicates.single.text=duplicate was deleted
+dialog.deleteduplicates.multi.text=duplicates were deleted
+dialog.deleteduplicates.nonefound=No duplicates found
+dialog.compresstrack.title=Compress Track
+dialog.compresstrack.parameter.text=Parameter for compression (lower number = more compression)
+dialog.compresstrack.text=Track compressed
+dialog.compresstrack.single.text=data point was removed
+dialog.compresstrack.multi.text=data points were removed
+dialog.compresstrack.nonefound=No data points could be removed
+dialog.openoptions.title=Open options
+dialog.openoptions.filesnippet=Extract of fichier
+dialog.load.table.field=Field
+dialog.load.table.datatype=Data Type
+dialog.load.table.description=Description
+dialog.delimiter.label=Séparateur de texte
+dialog.delimiter.comma=Virgule ,
+dialog.delimiter.tab=Tabulation
+dialog.delimiter.space=Espace
+dialog.delimiter.semicolon=Point-virgule ;
+dialog.delimiter.other=Autres
+dialog.openoptions.deliminfo.records=records, avec 
+dialog.openoptions.deliminfo.fields=fields
+dialog.openoptions.deliminfo.norecords=Pas de records
+dialog.openoptions.tabledesc=Extract of fichier
+dialog.openoptions.altitudeunits=Unités de altitude
+dialog.jpegload.subdirectories=Subdirectories aussi
+dialog.jpegload.loadjpegswithoutcoords=Photos sans coordonnées aussi
+dialog.jpegload.progress.title=Loading photos
+dialog.jpegload.progress=Please wait while the photos are searched
+dialog.jpegload.title=Loaded photos
+dialog.jpegload.photoadded=photo was added
+dialog.jpegload.photosadded=photos were added
+dialog.saveoptions.title=Save fichier
+dialog.save.fieldstosave=Fields to save
+dialog.save.table.field=Field
+dialog.save.table.hasdata=Has data
+dialog.save.table.save=Enregistrer
+dialog.save.headerrow=Output header row
+dialog.save.coordinateunits=Unités des coordonnées
+dialog.save.units.original=Original
+dialog.save.altitudeunits=Unités de altitude
+dialog.save.oktitle=File saved
+dialog.save.ok1=Successfully saved
+dialog.save.ok2=points to fichier
+dialog.save.overwrite.title=File already exists
+dialog.save.overwrite.text=This fichier already exists. Are you sure you want to overwrite the fichier?
+dialog.exportkml.title=Exporter au KML
+dialog.exportkml.text=Title for the data
+dialog.exportkml.kmz=Compress to make kmz fichier
+dialog.exportkml.exportimages=Export image thumbnails to kmz
+dialog.exportkml.filetype=Classeur KML, KMZ
+dialog.exportpov.title=Exporter au POV
+dialog.exportpov.text=Please enter the parameters for the POV export
+dialog.exportpov.font=Police
+dialog.exportpov.camerax=Camera X
+dialog.exportpov.cameray=Camera Y
+dialog.exportpov.cameraz=Camera Z
+dialog.exportpov.filetype=Classeur POV
+dialog.exportpov.warningtracksize=This track has a large number of points, which Java3D might not be able to display.\nAre you sure you want to continue?
+dialog.confirmreversetrack.title=Confirm reversal
+dialog.confirmreversetrack.text=This track contains timestamp information, which will be out of sequence after a reversal.\nAre you sure you want to reverse this section?
+dialog.interpolate.title=Interpolate points
+dialog.interpolate.parameter.text=Number of points to insert between selected points
+dialog.undo.title=Undo action(s)
+dialog.undo.pretext=Please select the action(s) to undo
+dialog.confirmundo.title=Operation(s) undone
+dialog.confirmundo.single.text=operation undone.
+dialog.confirmundo.multiple.text=operations undone.
+dialog.undo.none.title=Cannot undo
+dialog.undo.none.text=No operations to undo!
+dialog.clearundo.title=Clear undo list
+dialog.clearundo.text=Are you sure you want to clear the undo list?\nAll undo information will be lost!
+dialog.pointedit.title=Edit point
+dialog.pointedit.text=Select each field to edit and use the 'Edit' button to change the value
+dialog.pointedit.table.field=Field
+dialog.pointedit.table.value=Value
+dialog.pointedit.table.changed=Changed
+dialog.pointedit.changevalue.text=Enter the new value for this field
+dialog.pointedit.changevalue.title=Edit field
+dialog.pointnameedit.title=Edit waypoint name
+dialog.pointnameedit.name=Waypoint name
+dialog.pointnameedit.uppercase=CASSE MAJUSCULES
+dialog.pointnameedit.lowercase=casse minuscules
+dialog.pointnameedit.sentencecase=Casse Sentence
+dialog.saveexif.title=Save Exif
+dialog.saveexif.intro=Select the photos to save using the checkboxes
+dialog.saveexif.nothingtosave=Coordinate data is unchanged, nothing to save
+dialog.saveexif.noexiftool=No exiftool program could be found. Continue?
+dialog.saveexif.table.photoname=Photo name
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Save
+dialog.saveexif.photostatus.connected=Connected
+dialog.saveexif.photostatus.disconnected=Disconnected
+dialog.saveexif.photostatus.modified=Modified
+dialog.saveexif.overwrite=Overwrite fichiers
+dialog.saveexif.ok1=Saved
+dialog.saveexif.ok2=photo fichiers
+dialog.about.title=À propos de Prune
+dialog.about.version=Version
+dialog.about.build=Build
+dialog.about.summarytext1=Prune est une programme for loading, displaying and editing data from GPS receivers.
+dialog.about.summarytext2=It is released under the Gnu GPL for free, open, worldwide use and enhancement.<br>Copying, redistribution and modification are permitted and encouraged<br>according to the conditions in the included <code>license.txt</code> file.
+dialog.about.summarytext3=Consultez la page <code style="font-weight:bold">http://activityworkshop.net/</code> pour de plus détails et user guides.
+dialog.about.translatedby=Texte en français de activityworkshop.
+dialog.about.systeminfo=Info de Systeme
+dialog.about.systeminfo.os=Operating Systeme
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d installed
+dialog.about.systeminfo.povray=Povray installed
+dialog.about.systeminfo.exiftool=Exiftool installed
+dialog.about.yes=Oui
+dialog.about.no=Non
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune code written by
+dialog.about.credits.exifcode=Exif code by
+dialog.about.credits.icons=Some icons taken from
+dialog.about.credits.translations=Translations helped by
+dialog.about.credits.devtools=Development tools
+dialog.about.credits.othertools=Other tools
+dialog.about.credits.thanks=Thanks to
+
+# 3d window
+dialog.3d.title=Vue Trois-d de Prune
+dialog.3d.altitudecap=Minimum altitude range
+dialog.3dlines.title=Prune gridlines
+dialog.3dlines.empty=No gridlines to display!
+dialog.3dlines.intro=These are the gridlines for the three-d view
+
+# Buttons
+button.ok=OK
+button.back=Retour
+button.next=Prochain
+button.finish=Fini
+button.cancel=Annuler
+button.overwrite=Overwrite
+button.moveup=Move up
+button.movedown=Move down
+button.deletepoint=Delete point
+button.deleterange=Delete range
+button.showlines=Show lines
+button.edit=Éditer
+button.exit=Terminer
+button.close=Fermer
+button.continue=Continuer
+button.yes=Oui
+button.no=Non
+button.yestoall=Oui pour tous
+button.notoall=Non pour tous
+button.selectall=Sélecter tous
+button.selectnone=Sélecter rien
+
+# Display components
+display.nodata=No data loaded
+display.noaltitudes=Track data does not include altitudes
+details.trackdetails=Track details
+details.notrack=No track loaded
+details.track.points=Points
+details.track.file=File
+details.track.numfiles=Number of fichiers
+details.pointdetails=Point details
+details.index.selected=Index
+details.index.of=de
+details.nopointselection=No point selected
+details.photofile=Photo fichier
+details.norangeselection=No range selected
+details.rangedetails=Range details
+details.range.selected=Selected
+details.range.to=à
+details.altitude.to=à
+details.range.climb=Montée
+details.range.descent=Descente
+details.distanceunits=Unités de distance
+display.range.time.secs=s
+display.range.time.mins=m
+display.range.time.hours=h
+display.range.time.days=j
+details.waypointsphotos.waypoints=Waypoints
+details.waypointsphotos.photos=Photos
+details.photodetails=Photo details
+details.nophoto=Pas de Photo
+details.photo.loading=Loading
+
+# Field names
+fieldname.latitude=Latitude
+fieldname.longitude=Longitude
+fieldname.altitude=Altitude
+fieldname.timestamp=Timestamp
+fieldname.waypointname=Nom
+fieldname.waypointtype=Type
+fieldname.newsegment=Segment
+fieldname.custom=Custom
+fieldname.prefix=Champ
+fieldname.distance=Distance
+fieldname.duration=Durée
+
+# Measurement units
+units.metres=mètres
+units.metres.short=m
+units.feet=pieds
+units.feet.short=p
+units.kilometres=Kilomètres
+units.kilometres.short=km
+units.miles=lieues
+units.miles.short=li
+units.degminsec=Deg-min-sec
+units.degmin=Deg-min
+units.deg=Degrés
+
+# Cardinals for 3d plots
+cardinal.n=N
+cardinal.s=S
+cardinal.e=E
+cardinal.w=O
+
+# Undo operations
+undo.load=load data
+undo.loadphotos=load photos
+undo.editpoint=editer point
+undo.deletepoint=delete point
+undo.deletephoto=remove photo
+undo.deleterange=delete range
+undo.compress=compress track
+undo.insert=insert points
+undo.deleteduplicates=delete duplicates
+undo.reverse=reverse range
+undo.rearrangewaypoints=rearrange waypoints
+undo.connectphoto=connect photo
+
+# Error messages
+error.save.dialogtitle=Error saving data
+error.save.nodata=No data to save
+error.save.failed=Failed to save the data to fichier:
+error.saveexif.filenotfound=Failed to find photo fichier
+error.saveexif.cannotoverwrite1=Photo fichier
+error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy?
+error.load.dialogtitle=Error loading data
+error.load.noread=Cannot read fichier
+error.load.nopoints=No coordinate information found in the fichier
+error.load.unknownxml=Unrecognised xml format:
+error.load.othererror=Error reading fichier:
+error.jpegload.dialogtitle=Error loading photos
+error.jpegload.nofilesfound=No fichiers found
+error.jpegload.nojpegsfound=No jpeg fichiers found
+error.jpegload.noexiffound=No EXIF information found
+error.jpegload.nogpsfound=No GPS information found
+error.undofailed.title=Undo failed
+error.undofailed.text=Failed to undo operation
+error.function.noop.title=Function had no effect
+error.rearrange.noop=Rearranging waypoints had no effect
+error.function.notimplemented=Desoler, this function has not yet been implemented.
+error.function.notavailable.title=Function not available
+error.function.nojava3d=This function requires the Java3d library,\navailable from Sun.com or Blackdown.org.
+error.3d.title=Error in 3d display
+error.3d=An error occurred with the 3d display
index 733754bd83c0d98cdab258020907f89f98017430..ad0f0efb9088855696c898178bee335f25bfadaf 100644 (file)
@@ -96,10 +96,15 @@ public class FileCacher
                {
                        for (int i=0; i<size; i++)
                        {
-                               if (result[i].length() > inMaxWidth)
-                                       result[i] = result[i].trim();
-                               if (result[i].length() > inMaxWidth)
-                                       result[i] = result[i].substring(0, inMaxWidth);
+                               if (result[i] == null)
+                                       result[i] = "";
+                               else
+                               {
+                                       if (result[i].length() > inMaxWidth)
+                                               result[i] = result[i].trim();
+                                       if (result[i].length() > inMaxWidth)
+                                               result[i] = result[i].substring(0, inMaxWidth);
+                               }
                        }
                }
                return result;
index c7126f584ecfcb04589671ba0238a33457d48167..c1bf3dfc1a8e05d202bf00923f2cb94fc37d1017 100644 (file)
@@ -1,91 +1,26 @@
 package tim.prune.load;
 
-import java.awt.BorderLayout;
-import java.awt.CardLayout;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.GridLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import javax.swing.*;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-import javax.swing.table.TableCellEditor;
-
 import java.io.File;
 
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+
 import tim.prune.App;
 import tim.prune.I18nManager;
-import tim.prune.data.Altitude;
-import tim.prune.data.Field;
+import tim.prune.load.xml.XmlFileLoader;
 
 
 /**
- * Special class to handle file loading including GUI options,
- * and conversion to a Track object
+ * Generic FileLoader class to select a file
+ * and pass handling on to appropriate loader
  */
 public class FileLoader
 {
-       private File _file = null;
-       private App _app = null;
-       private JFrame _parentFrame = null;
-       private JDialog _dialog = null;
-       private JPanel _cardPanel = null;
-       private CardLayout _layout = null;
-       private JButton _backButton = null, _nextButton = null;
-       private JButton _finishButton = null;
-       private JButton _moveUpButton = null, _moveDownButton = null;
-       private JRadioButton[] _delimiterRadios = null;
-       private JTextField _otherDelimiterText = null;
-       private JLabel _statusLabel = null;
-       private DelimiterInfo[] _delimiterInfos = null;
        private JFileChooser _fileChooser = null;
-       private FileCacher _fileCacher = null;
-       private JList _snippetBox = null;
-       private FileExtractTableModel _fileExtractTableModel = null;
-       private JTable _fieldTable;
-       private FieldSelectionTableModel _fieldTableModel = null;
-       private JComboBox _unitsDropDown = null;
-       private int _selectedField = -1;
-       private char _currentDelimiter = ',';
-
-       // previously selected values
-       private char _lastUsedDelimiter = ',';
-       private int _lastNumFields = -1;
-       private Field[] _lastSelectedFields = null;
-       private int _lastAltitudeFormat = Altitude.FORMAT_NONE;
-
-       // constants
-       private static final int SNIPPET_SIZE = 6;
-       private static final int MAX_SNIPPET_WIDTH = 80;
-       private static final char[] DELIMITERS = {',', '\t', ';', ' '};
-
-
-       /**
-        * Inner class to listen for delimiter change operations
-        */
-       private class DelimListener implements ActionListener, DocumentListener
-       {
-               public void actionPerformed(ActionEvent e)
-               {
-                       informDelimiterSelected();
-               }
-               public void changedUpdate(DocumentEvent e)
-               {
-                       informDelimiterSelected();
-               }
-               public void insertUpdate(DocumentEvent e)
-               {
-                       informDelimiterSelected();
-               }
-               public void removeUpdate(DocumentEvent e)
-               {
-                       informDelimiterSelected();
-               }
-       }
+       private JFrame _parentFrame;
+       private TextFileLoader _textFileLoader = null;
+       private XmlFileLoader _xmlFileLoader = null;
 
 
        /**
@@ -95,8 +30,9 @@ public class FileLoader
         */
        public FileLoader(App inApp, JFrame inParentFrame)
        {
-               _app = inApp;
                _parentFrame = inParentFrame;
+               _textFileLoader = new TextFileLoader(inApp, inParentFrame);
+               _xmlFileLoader = new XmlFileLoader(inApp, inParentFrame);
        }
 
 
@@ -110,454 +46,40 @@ public class FileLoader
                        _fileChooser = new JFileChooser();
                if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
                {
-                       _file = _fileChooser.getSelectedFile();
-                       if (preCheckFile(_file))
+                       File file = _fileChooser.getSelectedFile();
+                       // Check file exists and is readable
+                       if (file != null && file.exists() && file.canRead())
                        {
-                               _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
-                               _dialog.setLocationRelativeTo(_parentFrame);
-                               _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
-                               _dialog.getContentPane().add(makeDialogComponents());
-
-                               // select best separator according to row counts (more is better)
-                               int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
-                                       _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
-                                       _delimiterInfos[3].getNumWinningRecords());
-                               if (bestDelim >= 0)
-                                       _delimiterRadios[bestDelim].setSelected(true);
+                               // Check file type to see if it's xml or just normal text
+                               String fileExtension = file.getName().toLowerCase();
+                               if (fileExtension.length() > 4)
+                                       {fileExtension = fileExtension.substring(fileExtension.length() - 4);}
+                               if (fileExtension.equals(".kml") || fileExtension.equals(".gpx")
+                                       || fileExtension.equals(".xml"))
+                               {
+                                       // Use xml loader for kml, gpx and xml filenames
+                                       _xmlFileLoader.openFile(file);
+                               }
                                else
-                                       _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
-                               informDelimiterSelected();
-                               _dialog.pack();
-                               _dialog.show();
+                               {
+                                       // Use text loader for everything else
+                                       _textFileLoader.openFile(file);
+                               }
                        }
                        else
                        {
+                               // couldn't read file - show error message
                                JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"),
                                        I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                        }
                }
        }
 
-
-       /**
-        * Check the given file for readability and funny characters,
-        * and count the fields for the various separators
-        * @param inFile file to check
-        */
-       private boolean preCheckFile(File inFile)
-       {
-               // Check file exists and is readable
-               if (inFile == null || !inFile.exists() || !inFile.canRead())
-               {
-                       return false;
-               }
-               // Use a FileCacher to read the file into an array
-               _fileCacher = new FileCacher(inFile);
-
-               // Check each line of the file
-               String[] fileContents = _fileCacher.getContents();
-               boolean fileOK = true;
-               _delimiterInfos = new DelimiterInfo[5];
-               for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
-
-               String currLine = null;
-               String[] splitFields = null;
-               int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
-               for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
-               {
-                       currLine = fileContents[lineNum];
-                       // check for invalid characters
-                       if (currLine.indexOf('\0') >= 0) {fileOK = false;}
-                       // check for commas
-                       splitFields = currLine.split(",");
-                       commaFields = splitFields.length;
-                       if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
-                       _delimiterInfos[0].updateMaxFields(commaFields);
-                       // check for tabs
-                       splitFields = currLine.split("\t");
-                       tabFields = splitFields.length;
-                       if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
-                       _delimiterInfos[1].updateMaxFields(tabFields);
-                       // check for semicolons
-                       splitFields = currLine.split(";");
-                       semicolonFields = splitFields.length;
-                       if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
-                       _delimiterInfos[2].updateMaxFields(semicolonFields);
-                       // check for spaces
-                       splitFields = currLine.split(" ");
-                       spaceFields = splitFields.length;
-                       if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
-                       _delimiterInfos[3].updateMaxFields(spaceFields);
-                       // increment counters
-                       int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
-                       if (bestScorer >= 0)
-                               _delimiterInfos[bestScorer].incrementNumWinningRecords();
-               }
-               return fileOK;
-       }
-
-
-       /**
-        * Get the index of the best one in the list
-        * @return the index of the maximum of the four given values
-        */
-       private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
-       {
-               int bestIndex = -1;
-               int maxScore = 1;
-               if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
-               if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
-               if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
-               if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
-               return bestIndex;
-       }
-
-
-       /**
-        * Make the components for the open options dialog
-        * @return Component for all options
-        */
-       private Component makeDialogComponents()
-       {
-               JPanel wholePanel = new JPanel();
-               wholePanel.setLayout(new BorderLayout());
-
-               // add buttons to south
-               JPanel buttonPanel = new JPanel();
-               buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
-               _backButton = new JButton(I18nManager.getText("button.back"));
-               _backButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               _layout.previous(_cardPanel);
-                               _backButton.setEnabled(false);
-                               _nextButton.setEnabled(true);
-                               _finishButton.setEnabled(false);
-                       }
-               });
-               _backButton.setEnabled(false);
-               buttonPanel.add(_backButton);
-               _nextButton = new JButton(I18nManager.getText("button.next"));
-               _nextButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               prepareSecondPanel();
-                               _layout.next(_cardPanel);
-                               _nextButton.setEnabled(false);
-                               _backButton.setEnabled(true);
-                               _finishButton.setEnabled(true);
-                       }
-               });
-               buttonPanel.add(_nextButton);
-               _finishButton = new JButton(I18nManager.getText("button.finish"));
-               _finishButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               finished();
-                       }
-               });
-               _finishButton.setEnabled(false);
-               buttonPanel.add(_finishButton);
-               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
-               cancelButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               _dialog.dispose();
-                       }
-               });
-               buttonPanel.add(cancelButton);
-               wholePanel.add(buttonPanel, BorderLayout.SOUTH);
-
-               // Make the two cards, for delimiter and fields
-               _cardPanel = new JPanel();
-               _layout = new CardLayout();
-               _cardPanel.setLayout(_layout);
-               JPanel firstCard = new JPanel();
-               firstCard.setLayout(new BorderLayout());
-
-               JPanel delimsPanel = new JPanel();
-               delimsPanel.setLayout(new GridLayout(0, 2));
-               delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
-               delimsPanel.add(new JLabel("")); // blank label to go to next grid row
-               // radio buttons
-               _delimiterRadios = new JRadioButton[5];
-               _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
-               delimsPanel.add(_delimiterRadios[0]);
-               _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
-               delimsPanel.add(_delimiterRadios[1]);
-               _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
-               delimsPanel.add(_delimiterRadios[2]);
-               _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
-               delimsPanel.add(_delimiterRadios[3]);
-               JPanel otherPanel = new JPanel();
-               otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
-               _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
-               otherPanel.add(_delimiterRadios[4]);
-               _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
-               otherPanel.add(_otherDelimiterText);
-               // Group radio buttons
-               ButtonGroup delimGroup = new ButtonGroup();
-               DelimListener delimListener = new DelimListener();
-               for (int i=0; i<_delimiterRadios.length; i++)
-               {
-                       delimGroup.add(_delimiterRadios[i]);
-                       _delimiterRadios[i].addActionListener(delimListener);
-               }
-               _otherDelimiterText.getDocument().addDocumentListener(delimListener);
-               delimsPanel.add(new JLabel(""));
-               delimsPanel.add(otherPanel);
-               _statusLabel = new JLabel("");
-               delimsPanel.add(_statusLabel);
-               firstCard.add(delimsPanel, BorderLayout.SOUTH);
-               // load snippet to show first few lines
-               _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
-               _snippetBox.setEnabled(false);
-               firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
-
-               // Second screen, for field order selection
-               JPanel secondCard = new JPanel();
-               secondCard.setLayout(new BorderLayout());
-               // table for file contents
-               _fileExtractTableModel = new FileExtractTableModel();
-               JTable extractTable = new JTable(_fileExtractTableModel);
-               JScrollPane tableScrollPane = new JScrollPane(extractTable);
-               extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
-               extractTable.getTableHeader().setReorderingAllowed(false);
-               secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH);
-               JPanel innerPanel2 = new JPanel();
-               innerPanel2.setLayout(new BorderLayout());
-               _fieldTable = new JTable(new FieldSelectionTableModel());
-               _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-               // add listener for selected table row
-               _fieldTable.getSelectionModel().addListSelectionListener(
-                       new ListSelectionListener() {
-                               public void valueChanged(ListSelectionEvent e) {
-                                       ListSelectionModel lsm = (ListSelectionModel) e.getSource();
-                                       if (lsm.isSelectionEmpty()) {
-                                               //no rows are selected
-                                               selectField(-1);
-                                       } else {
-                                               selectField(lsm.getMinSelectionIndex());
-                                       }
-                               }
-                       });
-               JPanel tablePanel = new JPanel();
-               tablePanel.setLayout(new BorderLayout());
-               tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
-               tablePanel.add(_fieldTable, BorderLayout.CENTER);
-               innerPanel2.add(tablePanel, BorderLayout.CENTER);
-
-               JPanel innerPanel3 = new JPanel();
-               innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
-               _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
-               _moveUpButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               int currRow = _fieldTable.getSelectedRow();
-                               closeTableComboBox(currRow);
-                               _fieldTableModel.moveUp(currRow);
-                               _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
-                       }
-               });
-               innerPanel3.add(_moveUpButton);
-               _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
-               _moveDownButton.addActionListener(new ActionListener() {
-                       public void actionPerformed(ActionEvent e)
-                       {
-                               int currRow = _fieldTable.getSelectedRow();
-                               closeTableComboBox(currRow);
-                               _fieldTableModel.moveDown(currRow);
-                               _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
-                       }
-               });
-               innerPanel3.add(_moveDownButton);
-               innerPanel3.add(Box.createVerticalStrut(70));
-
-               innerPanel2.add(innerPanel3, BorderLayout.EAST);
-               secondCard.add(innerPanel2, BorderLayout.CENTER);
-               JPanel altUnitsPanel = new JPanel();
-               altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
-               altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
-               String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
-               _unitsDropDown = new JComboBox(units);
-               altUnitsPanel.add(_unitsDropDown);
-               secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
-               _cardPanel.add(firstCard, "card1");
-               _cardPanel.add(secondCard, "card2");
-
-               wholePanel.add(_cardPanel, BorderLayout.CENTER);
-               return wholePanel;
-       }
-
-
-       /**
-        * Close the combo box on the selected row of the field table
-        * @param inRow currently selected row number
-        */
-       private void closeTableComboBox(int inRow)
-       {
-               TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
-               if (editor != null)
-               {
-                       editor.stopCellEditing();
-               }
-       }
-
-
-       /**
-        * change the status based on selection of a delimiter
-        */
-       protected void informDelimiterSelected()
-       {
-               for (int i=0; i<(_delimiterRadios.length-1); i++)
-               {
-                       if (_delimiterRadios[i].isSelected())
-                       {
-                               int numRecords = _delimiterInfos[i].getNumRecords();
-                               if (numRecords == 0)
-                               {
-                                       _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
-                               }
-                               else
-                               {
-                                       _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
-                                               + _delimiterInfos[i].getMaxFields() + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
-                               }
-                       }
-               }
-               if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
-               {
-                       _statusLabel.setText("");
-               }
-               // enable/disable next button
-               _nextButton.setEnabled(_delimiterRadios[4].isSelected() == false
-                       || _otherDelimiterText.getText().length() == 1);
-       }
-
-
-       /**
-        * Get the delimiter info from the first step
-        * @return delimiter information object for the selected delimiter
-        */
-       public DelimiterInfo getSelectedDelimiterInfo()
-       {
-               for (int i=0; i<4; i++)
-                       if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
-               // must be "other" - build info if necessary
-               if (_delimiterInfos[4] == null)
-                       _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
-               return _delimiterInfos[4];
-       }
-
-
-       /**
-        * Use the delimiter selected to determine the fields in the file
-        * and prepare the second panel accordingly
-        */
-       private void prepareSecondPanel()
-       {
-               DelimiterInfo info = getSelectedDelimiterInfo();
-               FileSplitter splitter = new FileSplitter(_fileCacher);
-               // Check info makes sense - num fields > 0, num records > 0
-               // set "Finished" button to disabled if not ok
-               // TODO: Work out if there are header rows or not, save?
-               // Try to match header rows with fields
-               // Try to match data with fields
-               // Add data to GUI elements
-               Object[][] tableData = splitter.splitFieldData(info.getDelimiter());
-               // possible to ignore blank columns here
-               _currentDelimiter = info.getDelimiter();
-               _fileExtractTableModel.updateData(tableData);
-               _fieldTableModel = new FieldSelectionTableModel();
-
-               // Check number of fields and use last ones if count matches
-               Field[] startFieldArray = null;
-               if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
-                       startFieldArray = _lastSelectedFields;
-               else
-                       startFieldArray = splitter.makeDefaultFields();
-               _fieldTableModel.updateData(startFieldArray);
-               _fieldTable.setModel(_fieldTableModel);
-               // add dropdowns to second column
-               JComboBox fieldTypesBox = new JComboBox();
-               for (int i=0; i<Field.ALL_AVAILABLE_FIELDS.length; i++)
-               {
-                       fieldTypesBox.addItem(Field.ALL_AVAILABLE_FIELDS[i].getName());
-               }
-               _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
-
-               // Set altitude format to same as last time if available
-               if (_lastAltitudeFormat == Altitude.FORMAT_METRES)
-                       _unitsDropDown.setSelectedIndex(0);
-               else if (_lastAltitudeFormat == Altitude.FORMAT_FEET)
-                       _unitsDropDown.setSelectedIndex(1);
-               // no selection on field list
-               selectField(-1);
-       }
-
-
-       /**
-        * All options have been selected, so load file
-        */
-       private void finished()
-       {
-               // Save delimiter, field array and altitude format for later use
-               _lastUsedDelimiter = _currentDelimiter;
-               _lastSelectedFields = _fieldTableModel.getFieldArray();
-               int altitudeFormat = Altitude.FORMAT_METRES;
-               if (_unitsDropDown.getSelectedIndex() == 1)
-               {
-                       altitudeFormat = Altitude.FORMAT_FEET;
-               }
-               _lastAltitudeFormat = altitudeFormat;
-               // give data to App
-               _app.informDataLoaded(_fieldTableModel.getFieldArray(),
-                       _fileExtractTableModel.getData(), altitudeFormat,
-                       _file.getName());
-               // clear up file cacher
-               _fileCacher.clear();
-               // dispose of dialog
-               _dialog.dispose();
-       }
-
-
-       /**
-        * Make a panel with a label and a component
-        * @param inLabelKey label key to use
-        * @param inComponent component for main area of panel
-        * @return labelled Panel
-        */
-       private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
-       {
-               JPanel panel = new JPanel();
-               panel.setLayout(new BorderLayout());
-               panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
-               panel.add(inComponent, BorderLayout.CENTER);
-               return panel;
-       }
-
-
-       /**
-        * An entry in the field list has been selected
-        * @param inFieldNum index of field, starting with 0
-        */
-       private void selectField(int inFieldNum)
-       {
-               if (inFieldNum == -1 || inFieldNum != _selectedField)
-               {
-                       _selectedField = inFieldNum;
-                       _moveUpButton.setEnabled(inFieldNum > 0);
-                       _moveDownButton.setEnabled(inFieldNum >= 0
-                               && inFieldNum < (_fieldTableModel.getRowCount()-1));
-               }
-       }
-
-
        /**
-        * @return the last delimiter character used for a load
+        * @return the last delimiter character used for a text file load
         */
        public char getLastUsedDelimiter()
        {
-               return _lastUsedDelimiter;
+               return _textFileLoader.getLastUsedDelimiter();
        }
 }
index 6fe52a18a96e48be328a83f5bb0320cd4c463fce..e6917c1a48c2500f846e78cdd1338a97f000e3c0 100644 (file)
@@ -24,6 +24,8 @@ import tim.prune.data.DataPoint;
 import tim.prune.data.Latitude;
 import tim.prune.data.Longitude;
 import tim.prune.data.Photo;
+import tim.prune.data.PhotoStatus;
+import tim.prune.data.Timestamp;
 import tim.prune.drew.jpeg.ExifReader;
 import tim.prune.drew.jpeg.JpegData;
 import tim.prune.drew.jpeg.JpegException;
@@ -38,6 +40,7 @@ public class JpegLoader implements Runnable
        private JFrame _parentFrame = null;
        private JFileChooser _fileChooser = null;
        private JCheckBox _subdirCheckbox = null;
+       private JCheckBox _noExifCheckbox = null;
        private JDialog _progressDialog   = null;
        private JProgressBar _progressBar = null;
        private int[] _fileCounts = null;
@@ -56,6 +59,7 @@ public class JpegLoader implements Runnable
                _parentFrame = inParentFrame;
        }
 
+
        /**
         * Select an input file and open the GUI frame
         * to select load options
@@ -67,9 +71,16 @@ public class JpegLoader implements Runnable
                        _fileChooser = new JFileChooser();
                        _fileChooser.setMultiSelectionEnabled(true);
                        _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+                       _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos"));
                        _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories"));
                        _subdirCheckbox.setSelected(true);
-                       _fileChooser.setAccessory(_subdirCheckbox);
+                       _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords"));
+                       _noExifCheckbox.setSelected(true);
+                       JPanel panel = new JPanel();
+                       panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+                       panel.add(_subdirCheckbox);
+                       panel.add(_noExifCheckbox);
+                       _fileChooser.setAccessory(panel);
                }
                if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
                {
@@ -118,40 +129,50 @@ public class JpegLoader implements Runnable
                // Initialise arrays, errors, summaries
                _fileCounts = new int[4]; // files, jpegs, exifs, gps
                _photos = new ArrayList();
-               // Loop over selected files/directories
                File[] files = _fileChooser.getSelectedFiles();
+               // Loop recursively over selected files/directories to count files
                int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
-               // if (false) System.out.println("Found " + numFiles + " files");
+               // Set up the progress bar for this number of files
                _progressBar.setMaximum(numFiles);
                _progressBar.setValue(0);
                _cancelled = false;
+
+               // Process the files recursively and build lists of photos
                processFileList(files, true, _subdirCheckbox.isSelected());
                _progressDialog.hide();
-               if (_cancelled) return;
-               // System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1] + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
+               if (_cancelled) {return;}
+
+               //System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1]
+               //  + ", " + _fileCounts[2] + ", " + _fileCounts[3]);
                if (_fileCounts[0] == 0)
                {
+                       // No files found at all
                        JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"),
                                I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                }
                else if (_fileCounts[1] == 0)
                {
+                       // No jpegs found
                        JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"),
                                I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                }
-               else if (_fileCounts[2] == 0)
+               else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0)
                {
+                       // Need coordinates but no exif found
                        JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.noexiffound"),
                                I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                }
-               else if (_fileCounts[3] == 0)
+               else if (!_noExifCheckbox.isSelected() && _fileCounts[3] == 0)
                {
+                       // Need coordinates but no gps information found
                        JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nogpsfound"),
                                I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                }
                else
                {
-                       // Load information into dialog for confirmation
+                       // Found some photos to load
+                       // TODO: Load jpeg information into dialog for confirmation?
+                       // Pass information back to app
                        _app.informPhotosLoaded(_photos);
                }
        }
@@ -182,14 +203,15 @@ public class JpegLoader implements Runnable
                                        {
                                                // Always process first directory,
                                                // only process subdirectories if checkbox selected
-                                               processDirectory(file, inDescend);
+                                               File[] files = file.listFiles();
+                                               processFileList(files, false, inDescend);
                                        }
                                }
                                else
                                {
-                                       // file doesn't exist or isn't readable - record error
+                                       // file doesn't exist or isn't readable - ignore error
                                }
-                               // check for cancel
+                               // check for cancel button pressed
                                if (_cancelled) break;
                        }
                }
@@ -202,52 +224,55 @@ public class JpegLoader implements Runnable
         */
        private void processFile(File inFile)
        {
+               // Update progress bar
                _fileCounts[0]++; // file found
                _progressBar.setValue(_fileCounts[0]);
                _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum());
                _progressBar.repaint();
+
+               // Check whether filename corresponds with accepted filenames
+               if (!acceptPhotoFilename(inFile.getName())) {return;}
+
+               // Create Photo object
+               Photo photo = new Photo(inFile);
+               // Try to get information out of exif
                try
                {
                        JpegData jpegData = new ExifReader(inFile).extract();
                        _fileCounts[1]++; // jpeg found (no exception thrown)
-//                     if (jpegData.getNumErrors() > 0)
-//                             System.out.println("Number of errors was: " + jpegData.getNumErrors() + ": " + jpegData.getErrors().get(0));
                        if (jpegData.getExifDataPresent())
-                               _fileCounts[2]++; // exif found
+                               {_fileCounts[2]++;} // exif found
                        if (jpegData.isValid())
                        {
-//                             if (false && jpegData.getTimestamp() != null)
-//                                     System.out.println("Timestamp is " + jpegData.getTimestamp()[0].toString() + ":" + jpegData.getTimestamp()[1].toString() + ":" + jpegData.getTimestamp()[2].toString());
-//                             if (false && jpegData.getDatestamp() != null)
-//                                     System.out.println("Datestamp is " + jpegData.getDatestamp()[0].toString() + ":" + jpegData.getDatestamp()[1].toString() + ":" + jpegData.getDatestamp()[2].toString());
-                               // Make DataPoint and Photo
+                               if (jpegData.getDatestamp() != null && jpegData.getTimestamp() != null)
+                               {
+                                       photo.setTimestamp(createTimestamp(jpegData.getDatestamp(), jpegData.getTimestamp()));
+                               }
+                               // Make DataPoint and attach to Photo
                                DataPoint point = createDataPoint(jpegData);
-                               Photo photo = new Photo(inFile);
                                point.setPhoto(photo);
                                photo.setDataPoint(point);
-                               _photos.add(photo);
-//                             System.out.println("Made photo: " + photo.getFile().getAbsolutePath() + " with the datapoint: "
-//                                     + point.getLatitude().output(Latitude.FORMAT_DEG_MIN_SEC) + ", "
-//                                     + point.getLongitude().output(Longitude.FORMAT_DEG_MIN_SEC) + ", "
-//                                     + point.getAltitude().getValue(Altitude.FORMAT_METRES));
+                               photo.setOriginalStatus(PhotoStatus.TAGGED);
                                _fileCounts[3]++;
                        }
                }
                catch (JpegException jpe) { // don't list errors, just count them
                }
-       }
-
-
-       /**
-        * Process the given directory, by looping over its contents
-        * and recursively through its subdirectories
-        * @param inDirectory directory to read
-        * @param inDescend true to descend subdirectories
-        */
-       private void processDirectory(File inDirectory, boolean inDescend)
-       {
-               File[] files = inDirectory.listFiles();
-               processFileList(files, false, inDescend);
+               // Use file timestamp if exif timestamp isn't available
+               if (photo.getTimestamp() == null)
+               {
+                       photo.setTimestamp(new Timestamp(inFile.lastModified()));
+                       //System.out.println("No exif, using timestamp from file: " + inFile.lastModified() + " -> " + photo.getTimestamp().getText());
+               }
+               else
+               {
+                       //System.out.println("timestamp from file = " + photo.getTimestamp().getText());
+               }
+               // Add the photo if it's got a point or if pointless photos should be added
+               if (photo.getDataPoint() != null || _noExifCheckbox.isSelected())
+               {
+                       _photos.add(photo);
+               }
        }
 
 
@@ -295,11 +320,15 @@ public class JpegLoader implements Runnable
                // Create model objects from jpeg data
                double latval = getCoordinateDoubleValue(inData.getLatitude(),
                        inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n');
-               Latitude latitude = new Latitude(latval, Latitude.FORMAT_NONE);
+               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_NONE);
-               Altitude altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES);
+               Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC);
+               Altitude altitude = null;
+               if (inData.getAltitude() != null)
+               {
+                       altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES);
+               }
                return new DataPoint(latitude, longitude, altitude);
        }
 
@@ -321,4 +350,49 @@ public class JpegLoader implements Runnable
                if (!isPositive) value = -value;
                return value;
        }
+
+
+       /**
+        * Use the given Rational values to create a timestamp
+        * @param inDate rationals describing date
+        * @param inTime rationals describing time
+        * @return Timestamp object corresponding to inputs
+        */
+       private static Timestamp createTimestamp(Rational[] inDate, Rational[] inTime)
+       {
+               //System.out.println("Making timestamp for date (" + inDate[0].toString() + "," + inDate[1].toString() + "," + inDate[2].toString() + ") and time ("
+               //      + inTime[0].toString() + "," + inTime[1].toString() + "," + inTime[2].toString() + ")");
+               return new Timestamp(inDate[0].intValue(), inDate[1].intValue(), inDate[2].intValue(),
+                       inTime[0].intValue(), inTime[1].intValue(), inTime[2].intValue());
+       }
+
+
+       /**
+        * Check whether to accept the given filename
+        * @param inName name of file
+        * @return true if accepted, false otherwise
+        */
+       private static boolean acceptPhotoFilename(String inName)
+       {
+               if (inName != null && inName.length() > 4)
+               {
+                       // Check for three-character file extensions jpg and jpe
+                       String lastFour = inName.substring(inName.length() - 4).toLowerCase();
+                       if (lastFour.equals(".jpg") || lastFour.equals(".jpe"))
+                       {
+                               return true;
+                       }
+                       // If not found, check for file extension jpeg
+                       if (inName.length() > 5)
+                       {
+                               String lastFive = inName.substring(inName.length() - 5).toLowerCase();
+                               if (lastFive.equals(".jpeg"))
+                               {
+                                       return true;
+                               }
+                       }
+               }
+               // Not matched so don't accept
+               return false;
+       }
 }
diff --git a/tim/prune/load/PhotoMeasurer.java b/tim/prune/load/PhotoMeasurer.java
new file mode 100644 (file)
index 0000000..710508a
--- /dev/null
@@ -0,0 +1,60 @@
+package tim.prune.load;
+
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoList;
+
+/**
+ * This class starts a new thread to preload image sizes
+ * TODO: # Cache small image thumbnails too?
+ */
+public class PhotoMeasurer implements Runnable
+{
+       /** PhotoList to loop through */
+       private PhotoList _photoList = null;
+
+
+       /**
+        * Constructor
+        * @param inPhotoList photo list to loop through
+        */
+       public PhotoMeasurer(PhotoList inPhotoList)
+       {
+               _photoList = inPhotoList;
+       }
+
+
+       /**
+        * Start off the process to measure the photo sizes
+        */
+       public void measurePhotos()
+       {
+               // check if any photos in list
+               if (_photoList != null && _photoList.getNumPhotos() > 0)
+               {
+                       // start new thread
+                       new Thread(this).start();
+               }
+       }
+
+
+       /**
+        * Run method called in new thread
+        */
+       public void run()
+       {
+               try
+               {
+                       // loop over all photos in list
+                       for (int i=0; i<_photoList.getNumPhotos(); i++)
+                       {
+                               Photo photo = _photoList.getPhoto(i);
+                               if (photo != null)
+                               {
+                                       // call get size method which will calculate it if necessary
+                                       photo.getSize();
+                               }
+                       }
+               }
+               catch (ArrayIndexOutOfBoundsException obe) {} // ignore, must have been changed by other thread
+       }
+}
diff --git a/tim/prune/load/TextFileLoader.java b/tim/prune/load/TextFileLoader.java
new file mode 100644 (file)
index 0000000..b692631
--- /dev/null
@@ -0,0 +1,561 @@
+package tim.prune.load;
+
+import java.awt.BorderLayout;
+import java.awt.CardLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellEditor;
+
+import java.io.File;
+
+import tim.prune.App;
+import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
+import tim.prune.data.Field;
+
+
+/**
+ * Class to handle loading of text files including GUI options,
+ * and passing loaded data back to App object
+ */
+public class TextFileLoader
+{
+       private File _file = null;
+       private App _app = null;
+       private JFrame _parentFrame = null;
+       private JDialog _dialog = null;
+       private JPanel _cardPanel = null;
+       private CardLayout _layout = null;
+       private JButton _backButton = null, _nextButton = null;
+       private JButton _finishButton = null;
+       private JButton _moveUpButton = null, _moveDownButton = null;
+       private JRadioButton[] _delimiterRadios = null;
+       private JTextField _otherDelimiterText = null;
+       private JLabel _statusLabel = null;
+       private DelimiterInfo[] _delimiterInfos = null;
+       private FileCacher _fileCacher = null;
+       private JList _snippetBox = null;
+       private FileExtractTableModel _fileExtractTableModel = null;
+       private JTable _fieldTable;
+       private FieldSelectionTableModel _fieldTableModel = null;
+       private JComboBox _unitsDropDown = null;
+       private int _selectedField = -1;
+       private char _currentDelimiter = ',';
+
+       // previously selected values
+       private char _lastUsedDelimiter = ',';
+       private Field[] _lastSelectedFields = null;
+       private int _lastAltitudeFormat = Altitude.FORMAT_NONE;
+
+       // constants
+       private static final int SNIPPET_SIZE = 6;
+       private static final int MAX_SNIPPET_WIDTH = 80;
+       private static final char[] DELIMITERS = {',', '\t', ';', ' '};
+
+
+       /**
+        * Inner class to listen for delimiter change operations
+        */
+       private class DelimListener implements ActionListener, DocumentListener
+       {
+               public void actionPerformed(ActionEvent e)
+               {
+                       informDelimiterSelected();
+               }
+               public void changedUpdate(DocumentEvent e)
+               {
+                       informDelimiterSelected();
+               }
+               public void insertUpdate(DocumentEvent e)
+               {
+                       informDelimiterSelected();
+               }
+               public void removeUpdate(DocumentEvent e)
+               {
+                       informDelimiterSelected();
+               }
+       }
+
+
+       /**
+        * Constructor
+        * @param inApp Application object to inform of track load
+        * @param inParentFrame parent frame to reference for dialogs
+        */
+       public TextFileLoader(App inApp, JFrame inParentFrame)
+       {
+               _app = inApp;
+               _parentFrame = inParentFrame;
+       }
+
+
+       /**
+        * Open the selected file and show the GUI dialog
+        * to select load options
+        */
+       public void openFile(File inFile)
+       {
+               _file = inFile;
+               if (preCheckFile(_file))
+               {
+                       _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
+                       _dialog.setLocationRelativeTo(_parentFrame);
+                       _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+                       _dialog.getContentPane().add(makeDialogComponents());
+
+                       // select best separator according to row counts (more is better)
+                       int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
+                               _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
+                               _delimiterInfos[3].getNumWinningRecords());
+                       if (bestDelim >= 0)
+                               _delimiterRadios[bestDelim].setSelected(true);
+                       else
+                               _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
+                       informDelimiterSelected();
+                       _dialog.pack();
+                       _dialog.show();
+               }
+               else
+               {
+                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"),
+                               I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+
+       /**
+        * Check the given file for readability and funny characters,
+        * and count the fields for the various separators
+        * @param inFile file to check
+        */
+       private boolean preCheckFile(File inFile)
+       {
+               // Check file exists and is readable
+               if (inFile == null || !inFile.exists() || !inFile.canRead())
+               {
+                       return false;
+               }
+               // Use a FileCacher to read the file into an array
+               _fileCacher = new FileCacher(inFile);
+
+               // Check each line of the file
+               String[] fileContents = _fileCacher.getContents();
+               boolean fileOK = true;
+               _delimiterInfos = new DelimiterInfo[5];
+               for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
+
+               String currLine = null;
+               String[] splitFields = null;
+               int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
+               for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
+               {
+                       currLine = fileContents[lineNum];
+                       // check for invalid characters
+                       if (currLine.indexOf('\0') >= 0) {fileOK = false;}
+                       // check for commas
+                       splitFields = currLine.split(",");
+                       commaFields = splitFields.length;
+                       if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
+                       _delimiterInfos[0].updateMaxFields(commaFields);
+                       // check for tabs
+                       splitFields = currLine.split("\t");
+                       tabFields = splitFields.length;
+                       if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
+                       _delimiterInfos[1].updateMaxFields(tabFields);
+                       // check for semicolons
+                       splitFields = currLine.split(";");
+                       semicolonFields = splitFields.length;
+                       if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
+                       _delimiterInfos[2].updateMaxFields(semicolonFields);
+                       // check for spaces
+                       splitFields = currLine.split(" ");
+                       spaceFields = splitFields.length;
+                       if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
+                       _delimiterInfos[3].updateMaxFields(spaceFields);
+                       // increment counters
+                       int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
+                       if (bestScorer >= 0)
+                               _delimiterInfos[bestScorer].incrementNumWinningRecords();
+               }
+               return fileOK;
+       }
+
+
+       /**
+        * Get the index of the best one in the list
+        * @return the index of the maximum of the four given values
+        */
+       private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
+       {
+               int bestIndex = -1;
+               int maxScore = 1;
+               if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
+               if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
+               if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
+               if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
+               return bestIndex;
+       }
+
+
+       /**
+        * Make the components for the open options dialog
+        * @return Component for all options
+        */
+       private Component makeDialogComponents()
+       {
+               JPanel wholePanel = new JPanel();
+               wholePanel.setLayout(new BorderLayout());
+
+               // add buttons to south
+               JPanel buttonPanel = new JPanel();
+               buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
+               _backButton = new JButton(I18nManager.getText("button.back"));
+               _backButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _layout.previous(_cardPanel);
+                               _backButton.setEnabled(false);
+                               _nextButton.setEnabled(true);
+                               _finishButton.setEnabled(false);
+                       }
+               });
+               _backButton.setEnabled(false);
+               buttonPanel.add(_backButton);
+               _nextButton = new JButton(I18nManager.getText("button.next"));
+               _nextButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               prepareSecondPanel();
+                               _layout.next(_cardPanel);
+                               _nextButton.setEnabled(false);
+                               _backButton.setEnabled(true);
+                               _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1);
+                       }
+               });
+               buttonPanel.add(_nextButton);
+               _finishButton = new JButton(I18nManager.getText("button.finish"));
+               _finishButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               finished();
+                       }
+               });
+               _finishButton.setEnabled(false);
+               buttonPanel.add(_finishButton);
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _dialog.dispose();
+                       }
+               });
+               buttonPanel.add(cancelButton);
+               wholePanel.add(buttonPanel, BorderLayout.SOUTH);
+
+               // Make the two cards, for delimiter and fields
+               _cardPanel = new JPanel();
+               _layout = new CardLayout();
+               _cardPanel.setLayout(_layout);
+               JPanel firstCard = new JPanel();
+               firstCard.setLayout(new BorderLayout());
+
+               JPanel delimsPanel = new JPanel();
+               delimsPanel.setLayout(new GridLayout(0, 2));
+               delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
+               delimsPanel.add(new JLabel("")); // blank label to go to next grid row
+               // radio buttons
+               _delimiterRadios = new JRadioButton[5];
+               _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
+               delimsPanel.add(_delimiterRadios[0]);
+               _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
+               delimsPanel.add(_delimiterRadios[1]);
+               _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
+               delimsPanel.add(_delimiterRadios[2]);
+               _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
+               delimsPanel.add(_delimiterRadios[3]);
+               JPanel otherPanel = new JPanel();
+               otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
+               _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
+               otherPanel.add(_delimiterRadios[4]);
+               _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
+               otherPanel.add(_otherDelimiterText);
+               // Group radio buttons
+               ButtonGroup delimGroup = new ButtonGroup();
+               DelimListener delimListener = new DelimListener();
+               for (int i=0; i<_delimiterRadios.length; i++)
+               {
+                       delimGroup.add(_delimiterRadios[i]);
+                       _delimiterRadios[i].addActionListener(delimListener);
+               }
+               _otherDelimiterText.getDocument().addDocumentListener(delimListener);
+               delimsPanel.add(new JLabel(""));
+               delimsPanel.add(otherPanel);
+               _statusLabel = new JLabel("");
+               delimsPanel.add(_statusLabel);
+               firstCard.add(delimsPanel, BorderLayout.SOUTH);
+               // load snippet to show first few lines
+               _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
+               _snippetBox.setEnabled(false);
+               firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
+
+               // Second screen, for field order selection
+               JPanel secondCard = new JPanel();
+               secondCard.setLayout(new BorderLayout());
+               // table for file contents
+               _fileExtractTableModel = new FileExtractTableModel();
+               JTable extractTable = new JTable(_fileExtractTableModel);
+               JScrollPane tableScrollPane = new JScrollPane(extractTable);
+               extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
+               extractTable.getTableHeader().setReorderingAllowed(false);
+               secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH);
+               JPanel innerPanel2 = new JPanel();
+               innerPanel2.setLayout(new BorderLayout());
+               _fieldTable = new JTable(new FieldSelectionTableModel());
+               _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               // add listener for selected table row
+               _fieldTable.getSelectionModel().addListSelectionListener(
+                       new ListSelectionListener() {
+                               public void valueChanged(ListSelectionEvent e) {
+                                       ListSelectionModel lsm = (ListSelectionModel) e.getSource();
+                                       if (lsm.isSelectionEmpty()) {
+                                               //no rows are selected
+                                               selectField(-1);
+                                       } else {
+                                               selectField(lsm.getMinSelectionIndex());
+                                       }
+                               }
+                       });
+               JPanel tablePanel = new JPanel();
+               tablePanel.setLayout(new BorderLayout());
+               tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
+               tablePanel.add(_fieldTable, BorderLayout.CENTER);
+               innerPanel2.add(tablePanel, BorderLayout.CENTER);
+
+               JPanel innerPanel3 = new JPanel();
+               innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
+               _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
+               _moveUpButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               int currRow = _fieldTable.getSelectedRow();
+                               closeTableComboBox(currRow);
+                               _fieldTableModel.moveUp(currRow);
+                               _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
+                       }
+               });
+               innerPanel3.add(_moveUpButton);
+               _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
+               _moveDownButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               int currRow = _fieldTable.getSelectedRow();
+                               closeTableComboBox(currRow);
+                               _fieldTableModel.moveDown(currRow);
+                               _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
+                       }
+               });
+               innerPanel3.add(_moveDownButton);
+               innerPanel3.add(Box.createVerticalStrut(70));
+
+               innerPanel2.add(innerPanel3, BorderLayout.EAST);
+               secondCard.add(innerPanel2, BorderLayout.CENTER);
+               JPanel altUnitsPanel = new JPanel();
+               altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
+               altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
+               String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
+               _unitsDropDown = new JComboBox(units);
+               altUnitsPanel.add(_unitsDropDown);
+               secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
+               _cardPanel.add(firstCard, "card1");
+               _cardPanel.add(secondCard, "card2");
+
+               wholePanel.add(_cardPanel, BorderLayout.CENTER);
+               return wholePanel;
+       }
+
+
+       /**
+        * Close the combo box on the selected row of the field table
+        * @param inRow currently selected row number
+        */
+       private void closeTableComboBox(int inRow)
+       {
+               TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
+               if (editor != null)
+               {
+                       editor.stopCellEditing();
+               }
+       }
+
+
+       /**
+        * change the status based on selection of a delimiter
+        */
+       protected void informDelimiterSelected()
+       {
+               int fields = 0;
+               // Loop through radios to see which one is selected
+               for (int i=0; i<(_delimiterRadios.length-1); i++)
+               {
+                       if (_delimiterRadios[i].isSelected())
+                       {
+                               // Set label text to describe records and fields
+                               int numRecords = _delimiterInfos[i].getNumRecords();
+                               if (numRecords == 0)
+                               {
+                                       _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
+                               }
+                               else
+                               {
+                                       fields = _delimiterInfos[i].getMaxFields();
+                                       _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
+                                               + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
+                               }
+                       }
+               }
+               // Don't show label if "other" delimiter is chosen (as records, fields are unknown)
+               if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
+               {
+                       _statusLabel.setText("");
+               }
+               // enable/disable next button
+               _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1)
+                       || _otherDelimiterText.getText().length() == 1);
+       }
+
+
+       /**
+        * Get the delimiter info from the first step
+        * @return delimiter information object for the selected delimiter
+        */
+       public DelimiterInfo getSelectedDelimiterInfo()
+       {
+               for (int i=0; i<4; i++)
+                       if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
+               // must be "other" - build info if necessary
+               if (_delimiterInfos[4] == null)
+                       _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
+               return _delimiterInfos[4];
+       }
+
+
+       /**
+        * Use the delimiter selected to determine the fields in the file
+        * and prepare the second panel accordingly
+        */
+       private void prepareSecondPanel()
+       {
+               DelimiterInfo info = getSelectedDelimiterInfo();
+               FileSplitter splitter = new FileSplitter(_fileCacher);
+               // Check info makes sense - num fields > 0, num records > 0
+               // set "Finished" button to disabled if not ok
+               // TODO: Work out if there are header rows or not, save?
+               // Try to match header rows with fields
+               // Try to match data with fields
+               // Add data to GUI elements
+               Object[][] tableData = splitter.splitFieldData(info.getDelimiter());
+               // possible to ignore blank columns here
+               _currentDelimiter = info.getDelimiter();
+               _fileExtractTableModel.updateData(tableData);
+               _fieldTableModel = new FieldSelectionTableModel();
+
+               // Check number of fields and use last ones if count matches
+               Field[] startFieldArray = null;
+               if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
+                       startFieldArray = _lastSelectedFields;
+               else
+                       startFieldArray = splitter.makeDefaultFields();
+               _fieldTableModel.updateData(startFieldArray);
+               _fieldTable.setModel(_fieldTableModel);
+               // add dropdowns to second column
+               JComboBox fieldTypesBox = new JComboBox();
+               for (int i=0; i<Field.ALL_AVAILABLE_FIELDS.length; i++)
+               {
+                       fieldTypesBox.addItem(Field.ALL_AVAILABLE_FIELDS[i].getName());
+               }
+               _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
+
+               // Set altitude format to same as last time if available
+               if (_lastAltitudeFormat == Altitude.FORMAT_METRES)
+                       _unitsDropDown.setSelectedIndex(0);
+               else if (_lastAltitudeFormat == Altitude.FORMAT_FEET)
+                       _unitsDropDown.setSelectedIndex(1);
+               // no selection on field list
+               selectField(-1);
+       }
+
+
+       /**
+        * All options have been selected, so load file
+        */
+       private void finished()
+       {
+               // Save delimiter, field array and altitude format for later use
+               _lastUsedDelimiter = _currentDelimiter;
+               _lastSelectedFields = _fieldTableModel.getFieldArray();
+               int altitudeFormat = Altitude.FORMAT_METRES;
+               if (_unitsDropDown.getSelectedIndex() == 1)
+               {
+                       altitudeFormat = Altitude.FORMAT_FEET;
+               }
+               _lastAltitudeFormat = altitudeFormat;
+               // give data to App
+               _app.informDataLoaded(_fieldTableModel.getFieldArray(),
+                       _fileExtractTableModel.getData(), altitudeFormat,
+                       _file.getName());
+               // clear up file cacher
+               _fileCacher.clear();
+               // dispose of dialog
+               _dialog.dispose();
+       }
+
+
+       /**
+        * Make a panel with a label and a component
+        * @param inLabelKey label key to use
+        * @param inComponent component for main area of panel
+        * @return labelled Panel
+        */
+       private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
+       {
+               JPanel panel = new JPanel();
+               panel.setLayout(new BorderLayout());
+               panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
+               panel.add(inComponent, BorderLayout.CENTER);
+               return panel;
+       }
+
+
+       /**
+        * An entry in the field list has been selected
+        * @param inFieldNum index of field, starting with 0
+        */
+       private void selectField(int inFieldNum)
+       {
+               if (inFieldNum == -1 || inFieldNum != _selectedField)
+               {
+                       _selectedField = inFieldNum;
+                       _moveUpButton.setEnabled(inFieldNum > 0);
+                       _moveDownButton.setEnabled(inFieldNum >= 0
+                               && inFieldNum < (_fieldTableModel.getRowCount()-1));
+               }
+       }
+
+
+       /**
+        * @return the last delimiter character used for a load
+        */
+       public char getLastUsedDelimiter()
+       {
+               return _lastUsedDelimiter;
+       }
+}
diff --git a/tim/prune/load/xml/GpxHandler.java b/tim/prune/load/xml/GpxHandler.java
new file mode 100644 (file)
index 0000000..962b495
--- /dev/null
@@ -0,0 +1,157 @@
+package tim.prune.load.xml;
+
+import java.util.ArrayList;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import tim.prune.data.Field;
+
+
+/**
+ * Class for handling specifics of parsing Gpx files
+ */
+public class GpxHandler extends XmlHandler
+{
+       private boolean _insideName = false;
+       private boolean _insideElevation = false;
+       private boolean _insideTime = false;
+       private String _name = null, _latitude = null, _longitude = null;
+       private String _elevation = null;
+       private String _time = null;
+       private ArrayList _pointList = new ArrayList();
+
+
+       /**
+        * Receive the start of a tag
+        * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
+        */
+       public void startElement(String uri, String localName, String qName,
+                       Attributes attributes) throws SAXException
+       {
+               // Read the parameters for waypoints and track points
+               if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
+               {
+                       int numAttributes = attributes.getLength();
+                       for (int i=0; i<numAttributes; i++)
+                       {
+                               String att = attributes.getQName(i);
+                               if (att.equals("lat")) {_latitude = attributes.getValue(i);}
+                               else if (att.equals("lon")) {_longitude = attributes.getValue(i);}
+                       }
+                       _elevation = null;
+                       _name = null;
+                       _time = null;
+               }
+               else if (qName.equalsIgnoreCase("ele"))
+               {
+                       _insideElevation = true;
+               }
+               else if (qName.equalsIgnoreCase("name"))
+               {
+                       _insideName = true;
+               }
+               else if (qName.equalsIgnoreCase("time"))
+               {
+                       _insideTime = true;
+               }
+               super.startElement(uri, localName, qName, attributes);
+       }
+
+
+       /**
+        * Process end tag
+        * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
+        */
+       public void endElement(String uri, String localName, String qName)
+       throws SAXException
+       {
+               if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
+               {
+                       processPoint();
+               }
+               else if (qName.equalsIgnoreCase("ele"))
+               {
+                       _insideElevation = false;
+               }
+               else if (qName.equalsIgnoreCase("name"))
+               {
+                       _insideName = false;
+               }
+               else if (qName.equalsIgnoreCase("time"))
+               {
+                       _insideTime = false;
+               }
+               super.endElement(uri, localName, qName);
+       }
+
+
+       /**
+        * Process character text (inside tags or between them)
+        * @see org.xml.sax.ContentHandler#characters(char[], int, int)
+        */
+       public void characters(char[] ch, int start, int length)
+                       throws SAXException
+       {
+               String value = new String(ch, start, length);
+               if (_insideName) {_name = checkCharacters(_name, value);}
+               else if (_insideElevation) {_elevation = checkCharacters(_elevation, value);}
+               else if (_insideTime) {_time = checkCharacters(_time, value);}
+               super.characters(ch, start, length);
+       }
+
+
+       /**
+        * Check to concatenate partially-received values, if necessary
+        * @param inVariable variable containing characters received until now
+        * @param inValue new value received
+        * @return concatenation
+        */
+       private static String checkCharacters(String inVariable, String inValue)
+       {
+               if (inVariable == null) {return inValue;}
+               return inVariable + inValue;
+       }
+
+
+       /**
+        * Process a point, either a waypoint or track point
+        */
+       private void processPoint()
+       {
+               // Put the values into a String array matching the order in getFieldArray()
+               String[] values = new String[5];
+               values[0] = _latitude; values[1] = _longitude;
+               values[2] = _elevation; values[3] = _name;
+               values[4] = _time;
+               _pointList.add(values);
+       }
+
+
+       /**
+        * @see tim.prune.load.xml.XmlHandler#getFieldArray()
+        */
+       public Field[] getFieldArray()
+       {
+               final Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE,
+                       Field.WAYPT_NAME, Field.TIMESTAMP};
+               return fields;
+       }
+
+
+       /**
+        * Return the parsed information as a 2d array
+        * @see tim.prune.load.xml.XmlHandler#getDataArray()
+        */
+       public String[][] getDataArray()
+       {
+               int numPoints = _pointList.size();
+               // construct data array
+               String[][] result = new String[numPoints][];
+               for (int i=0; i<numPoints; i++)
+               {
+                       result[i] = (String[]) _pointList.get(i);
+               }
+               return result;
+       }
+}
diff --git a/tim/prune/load/xml/KmlHandler.java b/tim/prune/load/xml/KmlHandler.java
new file mode 100644 (file)
index 0000000..0970639
--- /dev/null
@@ -0,0 +1,150 @@
+package tim.prune.load.xml;
+
+import java.util.ArrayList;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import tim.prune.data.Field;
+
+
+/**
+ * Class for handling specifics of parsing Kml files
+ */
+public class KmlHandler extends XmlHandler
+{
+       private boolean _insidePlacemark = false;
+       private boolean _insideName = false;
+       private boolean _insideCoordinates = false;
+       private String _name = null;
+       private StringBuffer _coordinates = null;
+       private ArrayList _pointList = new ArrayList();
+
+
+       /**
+        * Receive the start of a tag
+        * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
+        */
+       public void startElement(String uri, String localName, String qName,
+                       Attributes attributes) throws SAXException
+       {
+               String tagName = localName;
+               if (tagName == null || tagName.equals("")) {tagName = qName;}
+               if (tagName.equalsIgnoreCase("Placemark")) _insidePlacemark = true;
+               else if (tagName.equalsIgnoreCase("coordinates")) {_insideCoordinates = true; _coordinates = null;}
+               else if (tagName.equalsIgnoreCase("name")) {_insideName = true; _name = null;}
+               super.startElement(uri, localName, qName, attributes);
+       }
+
+
+       /**
+        * Process end tag
+        * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
+        */
+       public void endElement(String uri, String localName, String qName)
+       throws SAXException
+       {
+               String tagName = localName;
+               if (tagName == null || tagName.equals("")) {tagName = qName;}
+               if (tagName.equalsIgnoreCase("Placemark"))
+               {
+                       processPlacemark();
+                       _insidePlacemark = false;
+               }
+               else if (tagName.equalsIgnoreCase("coordinates")) _insideCoordinates = false;
+               else if (tagName.equalsIgnoreCase("name")) _insideName = false;
+               super.endElement(uri, localName, qName);
+       }
+
+
+       /**
+        * Process character text (inside tags or between them)
+        * @see org.xml.sax.ContentHandler#characters(char[], int, int)
+        */
+       public void characters(char[] ch, int start, int length)
+                       throws SAXException
+       {
+               if (_insidePlacemark && (_insideName || _insideCoordinates))
+               {
+                       String value = new String(ch, start, length);
+                       if (_insideName) {_name = value;}
+                       else if (_insideCoordinates)
+                       {
+                               if (_coordinates == null)
+                               {
+                                       _coordinates = new StringBuffer();
+                               }
+                               _coordinates.append(value);
+                       }
+               }
+               super.characters(ch, start, length);
+       }
+
+
+       /**
+        * Process a placemark entry, either a single waypoint or a whole track
+        */
+       private void processPlacemark()
+       {
+               if (_coordinates == null) return;
+               String allCoords = _coordinates.toString();
+               String[] coordArray = allCoords.split("[ \n]");
+               int numPoints = coordArray.length;
+               if (numPoints == 1)
+               {
+                       // Add single waypoint to list
+                       _pointList.add(makeStringArray(allCoords, _name));
+               }
+               else if (numPoints > 1)
+               {
+                       // Add each of the unnamed track points to list
+                       for (int p=0; p<numPoints; p++)
+                       {
+                               _pointList.add(makeStringArray(coordArray[p], null));
+                       }
+               }
+       }
+
+
+       /**
+        * Construct the String array for the given coordinates and name
+        * @param inCoordinates coordinate string in Kml format
+        * @param inName name of waypoint, or null if track point
+        * @return String array for point
+        */
+       private static String[] makeStringArray(String inCoordinates, String inName)
+       {
+               String[] result = new String[4];
+               String[] values = inCoordinates.split(",");
+               if (values.length == 3) {System.arraycopy(values, 0, result, 0, 3);}
+               result[3] = inName;
+               return result;
+       }
+
+
+       /**
+        * @see tim.prune.load.xml.XmlHandler#getFieldArray()
+        */
+       public Field[] getFieldArray()
+       {
+               final Field[] fields = {Field.LONGITUDE, Field.LATITUDE, Field.ALTITUDE, Field.WAYPT_NAME};
+               return fields;
+       }
+
+
+       /**
+        * Return the parsed information as a 2d array
+        * @see tim.prune.load.xml.XmlHandler#getDataArray()
+        */
+       public String[][] getDataArray()
+       {
+               int numPoints = _pointList.size();
+               // construct data array
+               String[][] result = new String[numPoints][];
+               for (int i=0; i<numPoints; i++)
+               {
+                       result[i] = (String[]) _pointList.get(i);
+               }
+               return result;
+       }
+
+}
diff --git a/tim/prune/load/xml/XmlFileLoader.java b/tim/prune/load/xml/XmlFileLoader.java
new file mode 100644 (file)
index 0000000..a607b22
--- /dev/null
@@ -0,0 +1,148 @@
+package tim.prune.load.xml;
+
+import java.io.File;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+import tim.prune.App;
+import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
+
+/**
+ * Class for handling loading of Xml files, and passing the
+ * loaded data back to the App object
+ */
+public class XmlFileLoader extends DefaultHandler implements Runnable
+{
+       private File _file = null;
+       private App _app = null;
+       private JFrame _parentFrame = null;
+       private XmlHandler _handler = null;
+       private String _unknownType = null;
+
+
+       /**
+        * Constructor
+        * @param inApp Application object to inform of track load
+        * @param inParentFrame parent frame to reference for dialogs
+        */
+       public XmlFileLoader(App inApp, JFrame inParentFrame)
+       {
+               _app = inApp;
+               _parentFrame = inParentFrame;
+       }
+
+
+       /**
+        * Open the selected file and show the GUI dialog
+        * to select load options
+        */
+       public void openFile(File inFile)
+       {
+               _file = inFile;
+               _handler = null;
+               _unknownType = null;
+               // start new thread in case xml parsing is time-consuming
+               new Thread(this).start();
+       }
+
+
+       /**
+        * Run method, to parse the file
+        * @see java.lang.Runnable#run()
+        */
+       public void run()
+       {
+               try
+               {
+                       // Construct a SAXParser and use this as a default handler
+                       SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+                       saxParser.parse(_file, this);
+
+                       // Check whether handler was properly instantiated
+                       if (_handler == null)
+                       {
+                               // Wasn't either kml or gpx
+                               JOptionPane.showMessageDialog(_parentFrame,
+                                       I18nManager.getText("error.load.unknownxml") + " " + _unknownType,
+                                       I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+                       }
+                       else
+                       {
+                               // Pass information back to app
+                               _app.informDataLoaded(_handler.getFieldArray(), _handler.getDataArray(),
+                                       Altitude.FORMAT_METRES, _file.getName());
+                       }
+               }
+               catch (Exception e)
+               {
+                       // Show error dialog
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getText("error.load.othererror") + " " + e.getMessage(),
+                               I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+
+       /**
+        * Receive a tag
+        * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
+        */
+       public void startElement(String uri, String localName, String qName,
+                       Attributes attributes) throws SAXException
+       {
+               // Check for "kml" or "gpx" tags
+               if (_handler == null)
+               {
+                       if (qName.equals("kml")) {_handler = new KmlHandler();}
+                       else if (qName.equals("gpx")) {_handler = new GpxHandler();}
+                       else if (_unknownType == null && qName != null && !qName.equals(""))
+                       {
+                               _unknownType = qName;
+                       }
+               }
+               else
+               {
+                       // Handler instantiated so pass tags on to it
+                       _handler.startElement(uri, localName, qName, attributes);
+               }
+               super.startElement(uri, localName, qName, attributes);
+       }
+
+
+       /**
+        * Receive characters, either between or inside tags
+        * @see org.xml.sax.ContentHandler#characters(char[], int, int)
+        */
+       public void characters(char[] ch, int start, int length)
+                       throws SAXException
+       {
+               if (_handler != null)
+               {
+                       // Handler instantiated so pass tags on to it
+                       _handler.characters(ch, start, length);
+               }
+               super.characters(ch, start, length);
+       }
+
+
+       /**
+        * Receive end of element
+        * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
+        */
+       public void endElement(String uri, String localName, String qName)
+                       throws SAXException
+       {
+               if (_handler != null)
+               {
+                       // Handler instantiated so pass tags on to it
+                       _handler.endElement(uri, localName, qName);
+               }
+               super.endElement(uri, localName, qName);
+       }
+}
diff --git a/tim/prune/load/xml/XmlHandler.java b/tim/prune/load/xml/XmlHandler.java
new file mode 100644 (file)
index 0000000..954190a
--- /dev/null
@@ -0,0 +1,22 @@
+package tim.prune.load.xml;
+
+import org.xml.sax.helpers.DefaultHandler;
+
+import tim.prune.data.Field;
+
+/**
+ * Abstract superclass of xml handlers
+ */
+public abstract class XmlHandler extends DefaultHandler
+{
+       /**
+        * Method for returning data loaded from file
+        * @return 2d String array containing data
+        */
+       public abstract String[][] getDataArray();
+
+       /**
+        * @return field array describing fields of data
+        */
+       public abstract Field[] getFieldArray();
+}
index 4437095b128bfe04cadabbf4068fc84d896fac01..1f3143cf06085751001c938db47db65a00636c51 100644 (file)
@@ -1,4 +1,4 @@
-Prune version 2
+Prune version 3
 ===============
 
 Prune is an application for viewing, editing and managing coordinate data from GPS systems.
@@ -15,7 +15,7 @@ Running
 =======
 
 To run Prune from the jar file, simply call it from a command prompt or shell:
-   java -jar prune_02.jar
+   java -jar prune_03.jar
 
 If the jar file is saved in a different directory, you will need to include the path.
 Depending on your system settings, you may be able to click or double-click on the jar file
@@ -23,6 +23,17 @@ in a file manager window to execute it.  A shortcut, menu item, desktop icon or
 can of course be made should you wish.
 
 
+Updates since version 2
+=======================
+
+The following features were added since version 2:
+  - Loading of GPX and KML files
+  - Loading of jpeg photos with or without coordinate data
+  - Manual correlation of photos with points
+  - Saving of coordinates in exif data of jpegs
+  - Exporting to KMZ format including thumbnails of photos
+  - Four-panel layout with toolbar
+
 Updates since version 1
 =======================
 
diff --git a/tim/prune/save/ExifSaver.java b/tim/prune/save/ExifSaver.java
new file mode 100644 (file)
index 0000000..1e59720
--- /dev/null
@@ -0,0 +1,366 @@
+package tim.prune.save;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+
+import tim.prune.ExternalTools;
+import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
+import tim.prune.data.Coordinate;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoList;
+import tim.prune.data.PhotoStatus;
+
+/**
+ * Class to call Exiftool to save coordinate information in jpg files
+ */
+public class ExifSaver implements Runnable
+{
+       private Frame _parentFrame = null;
+       private JDialog _dialog = null;
+       private JCheckBox _overwriteCheckbox = null;
+       private JProgressBar _progressBar = null;
+       private PhotoTableModel _photoTableModel = null;
+
+
+       // To preserve timestamps of file use parameter -P
+       // To overwrite file (careful!) use parameter -overwrite_original_in_place
+
+       // To read all GPS tags,   use -GPS:All
+       // To delete all GPS tags, use -GPS:All=
+
+       // To set Altitude, use -GPSAltitude= and -GPSAltitudeRef=
+       // To set Latitude, use -GPSLatitude= and -GPSLatitudeRef=
+
+       // To delete all tags with overwrite: exiftool -P -overwrite_original_in_place -GPS:All= <filename>
+
+       // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' <filename>
+       // (setting altitude ref to 0 doesn't work)
+       // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N <filename>
+       // (latitude as space-separated deg min sec, reference as either N or S)
+       // Same for longitude, reference E or W
+
+
+       /**
+        * Constructor
+        * @param inParentFrame parent frame
+        */
+       public ExifSaver(Frame inParentFrame)
+       {
+               _parentFrame = inParentFrame;
+       }
+
+
+       /**
+        * Save exif information to all photos in the list
+        * whose coordinate information has changed since loading
+        * @param inPhotoList list of photos to save
+        */
+       public boolean saveExifInformation(PhotoList inPhotoList)
+       {
+               // Check if external exif tool can be called
+               boolean exifToolInstalled = ExternalTools.isExiftoolInstalled();
+               if (!exifToolInstalled)
+               {
+                       // show warning
+                       int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"),
+                               I18nManager.getText("dialog.saveexif.title"),
+                               JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+                       if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION)
+                       {
+                               return false;
+                       }
+               }
+               // Make model and add all photos to it
+               _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos());
+               for (int i=0; i<inPhotoList.getNumPhotos(); i++)
+               {
+                       Photo photo = inPhotoList.getPhoto(i);
+                       PhotoTableEntry entry = new PhotoTableEntry(photo);
+                       _photoTableModel.addPhotoInfo(entry);
+               }
+               // Check if there are any modified photos to save
+               if (_photoTableModel.getNumSaveablePhotos() < 1)
+               {
+                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.saveexif.nothingtosave"),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.WARNING_MESSAGE);
+                       return false;
+               }
+               // Construct dialog
+               _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveexif.title"), true);
+               _dialog.setLocationRelativeTo(_parentFrame);
+               _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+               _dialog.getContentPane().add(makeDialogComponents());
+               _dialog.pack();
+               // set progress bar and show dialog
+               _progressBar.setVisible(false);
+               _dialog.show();
+               return true;
+       }
+
+
+       /**
+        * Put together the dialog components for adding to the gui
+        * @return panel containing all gui components
+        */
+       private JPanel makeDialogComponents()
+       {
+               JPanel panel = new JPanel();
+               panel.setLayout(new BorderLayout());
+               panel.add(new JLabel(I18nManager.getText("dialog.saveexif.intro")), BorderLayout.NORTH);
+               // centre panel with most controls
+               JPanel centrePanel = new JPanel();
+               centrePanel.setLayout(new BorderLayout());
+               // table panel with table and checkbox
+               JPanel tablePanel = new JPanel();
+               tablePanel.setLayout(new BorderLayout());
+               JTable photoTable = new JTable(_photoTableModel);
+               JScrollPane scrollPane = new JScrollPane(photoTable);
+               scrollPane.setPreferredSize(new Dimension(300, 160));
+               tablePanel.add(scrollPane, BorderLayout.CENTER);
+               _overwriteCheckbox = new JCheckBox(I18nManager.getText("dialog.saveexif.overwrite"));
+               _overwriteCheckbox.setSelected(false);
+               tablePanel.add(_overwriteCheckbox, BorderLayout.SOUTH);
+               centrePanel.add(tablePanel, BorderLayout.CENTER);
+               // progress bar below main controls
+               _progressBar = new JProgressBar(0, 100);
+               centrePanel.add(_progressBar, BorderLayout.SOUTH);
+               panel.add(centrePanel, BorderLayout.CENTER);
+               // Right-hand panel with select all, none buttons
+               JPanel rightPanel = new JPanel();
+               rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS));
+               JButton selectAllButton = new JButton(I18nManager.getText("button.selectall"));
+               selectAllButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               selectPhotos(true);
+                       }
+               });
+               rightPanel.add(selectAllButton);
+               JButton selectNoneButton = new JButton(I18nManager.getText("button.selectnone"));
+               selectNoneButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               selectPhotos(false);
+                       }
+               });
+               rightPanel.add(selectNoneButton);
+               panel.add(rightPanel, BorderLayout.EAST);
+               // Lower panel with ok and cancel buttons
+               JPanel buttonPanel = new JPanel();
+               buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+               JButton okButton = new JButton(I18nManager.getText("button.ok"));
+               okButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // start new thread to do save
+                               new Thread(ExifSaver.this).start();
+                       }
+               });
+               buttonPanel.add(okButton);
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _dialog.dispose();
+                       }
+               });
+               buttonPanel.add(cancelButton);
+               panel.add(buttonPanel, BorderLayout.SOUTH);
+               return panel;
+       }
+
+
+       /**
+        * Select all or select none
+        * @param inSelected true to select all photos, false to deselect all
+        */
+       private void selectPhotos(boolean inSelected)
+       {
+               int numPhotos = _photoTableModel.getRowCount();
+               for (int i=0; i<numPhotos; i++)
+               {
+                       _photoTableModel.getPhotoTableEntry(i).setSaveFlag(inSelected);
+               }
+               _photoTableModel.fireTableDataChanged();
+       }
+
+
+       /**
+        * Run method for saving in separate thread
+        */
+       public void run()
+       {
+               PhotoTableEntry entry = null;
+               Photo photo = null;
+               int numPhotos = _photoTableModel.getRowCount();
+               _progressBar.setMaximum(numPhotos);
+               _progressBar.setValue(0);
+               _progressBar.setVisible(true);
+               boolean overwriteFlag = _overwriteCheckbox.isSelected();
+               int numSaved = 0;
+               // Loop over all photos in list
+               for (int i=0; i<numPhotos; i++)
+               {
+                       entry = _photoTableModel.getPhotoTableEntry(i);
+                       if (entry != null && entry.getSaveFlag())
+                       {
+                               // Only look at photos which are selected and whose status has changed since load
+                               photo = entry.getPhoto();
+                               if (photo != null && photo.getOriginalStatus() != photo.getCurrentStatus())
+                               {
+                                       // Increment counter if save successful
+                                       if (savePhoto(photo, overwriteFlag))
+                                       {
+                                               numSaved++;
+                                       }
+                               }
+                       }
+                       // update progress bar
+                       _progressBar.setValue(i + 1);
+               }
+               _progressBar.setVisible(false);
+               // Show confirmation dialog
+               JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.saveexif.ok1") + " "
+                       + numSaved + " " + I18nManager.getText("dialog.saveexif.ok2"),
+                       I18nManager.getText("dialog.saveexif.title"), JOptionPane.INFORMATION_MESSAGE);
+               // close dialog, all finished
+               _dialog.dispose();
+       }
+
+
+       /**
+        * Save the details for the given photo
+        * @param inPhoto Photo object
+        * @param inOverwriteFlag true to overwrite file, false otherwise
+        * @return true if details saved ok
+        */
+       private boolean savePhoto(Photo inPhoto, boolean inOverwriteFlag)
+       {
+               // Check whether photo file still exists
+               if (!inPhoto.getFile().exists())
+               {
+                       // photo file doesn't exist any more
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getText("error.saveexif.filenotfound") + " : " + inPhoto.getFile().getAbsolutePath(),
+                               I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
+                       return false;
+               }
+               // Warn if file read-only and selected to overwrite
+               if (inOverwriteFlag && !inPhoto.getFile().canWrite())
+               {
+                       // eek, can't overwrite file
+                       int answer = JOptionPane.showConfirmDialog(_parentFrame,
+                               I18nManager.getText("error.saveexif.cannotoverwrite1") + " " + inPhoto.getFile().getAbsolutePath()
+                                       + " " + I18nManager.getText("error.saveexif.cannotoverwrite2"),
+                               I18nManager.getText("dialog.saveexif.title"),
+                               JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE);
+                       if (answer == JOptionPane.YES_OPTION)
+                       {
+                               // don't overwrite this image but write to copy
+                               inOverwriteFlag = false;
+                       }
+                       else
+                       {
+                               // don't do anything with this file
+                               return false;
+                       }
+               }
+               String[] command = null;
+               if (inPhoto.getCurrentStatus() == PhotoStatus.NOT_CONNECTED)
+               {
+                       // Photo is no longer connected, so delete gps tags
+                       command = getDeleteGpsExifTagsCommand(inPhoto.getFile(), inOverwriteFlag);
+               }
+               else
+               {
+                       // Photo is now connected, so write new gps tags
+                       command = getWriteGpsExifTagsCommand(inPhoto.getFile(), inPhoto.getDataPoint(), inOverwriteFlag);
+               }
+               // Execute exif command
+               try
+               {
+                       Runtime.getRuntime().exec(command);
+               }
+               catch (Exception e)
+               {
+                       // show error message
+                       JOptionPane.showMessageDialog(_parentFrame, "Exception: '" + e.getClass().getName() + "' : "
+                               + e.getMessage(), I18nManager.getText("dialog.saveexif.title"), JOptionPane.ERROR_MESSAGE);
+                       return false;
+               }
+               return true;
+       }
+
+
+       /**
+        * Create the command to delete the gps exif tags from the specified file
+        * @param inFile file from which to delete tags
+        * @param inOverwrite true to overwrite file, false to create copy
+        * @return external command to delete gps tags
+        */
+       private static String[] getDeleteGpsExifTagsCommand(File inFile, boolean inOverwrite)
+       {
+               // Make a string array to construct the command and its parameters
+               String[] result = new String[inOverwrite?5:4];
+               result[0] = "exiftool";
+               result[1] = "-P";
+               if (inOverwrite) {result[2] = " -overwrite_original_in_place";}
+               // remove all gps tags
+               int paramOffset = inOverwrite?3:2;
+               result[paramOffset] = "-GPS:All=";
+               result[paramOffset + 1] = inFile.getAbsolutePath();
+               return result;
+       }
+
+
+       /**
+        * Create the comand to write the gps exif tags to the specified file
+        * @param inFile file to which to write the tags
+        * @param inPoint DataPoint object containing coordinate information
+        * @param inOverwrite true to overwrite file, false to create copy
+        * @return external command to write gps tags
+        */
+       private static String[] getWriteGpsExifTagsCommand(File inFile, DataPoint inPoint, boolean inOverwrite)
+       {
+               // Make a string array to construct the command and its parameters
+               String[] result = new String[inOverwrite?10:9];
+               result[0] = "exiftool";
+               result[1] = "-P";
+               if (inOverwrite) {result[2] = "-overwrite_original_in_place";}
+               int paramOffset = inOverwrite?3:2;
+               // To set latitude : -GPSLatitude='12 34 56.78' -GPSLatitudeRef='N'
+               // (latitude as space-separated deg min sec, reference as either N or S)
+               result[paramOffset] = "-GPSLatitude='" + inPoint.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
+                + "'";
+               result[paramOffset + 1] = "-GPSLatitudeRef=" + inPoint.getLatitude().output(Coordinate.FORMAT_CARDINAL);
+               // same for longitude with space-separated deg min sec, reference as either E or W
+               result[paramOffset + 2] = "-GPSLongitude='" + inPoint.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC_WITH_SPACES)
+                + "'";
+               result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL);
+               // add altitude if it has it
+               result[paramOffset + 4] = "-GPSAltitude="
+                + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.FORMAT_METRES):0);
+               result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'";
+               // add the filename to modify
+               result[paramOffset + 6] = inFile.getAbsolutePath();
+               return result;
+       }
+}
index 16624ad1495dd26c549a0af26a3c087fbb0364c7..11474a6c8e8fb37f911f53f3f79e51edebc5fcf4 100644 (file)
 package tim.prune.save;
 
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
 import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.Writer;
+import java.util.Iterator;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
+import javax.swing.JLabel;
 import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
 import javax.swing.filechooser.FileFilter;
 
-import tim.prune.App;
 import tim.prune.I18nManager;
 import tim.prune.data.Coordinate;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Track;
+import tim.prune.data.TrackInfo;
+import tim.prune.gui.ImageUtils;
 
 /**
  * Class to export track information
  * into a specified Kml file
  */
-public class KmlExporter
+public class KmlExporter implements Runnable
 {
-       private App _app = null;
        private JFrame _parentFrame = null;
+       private TrackInfo _trackInfo = null;
        private Track _track = null;
+       private JDialog _dialog = null;
+       private JTextField _descriptionField = null;
+       private JCheckBox _kmzCheckbox = null;
+       private JCheckBox _exportImagesCheckbox = null;
+       private JProgressBar _progressBar = null;
        private JFileChooser _fileChooser = null;
+       private File _exportFile = null;
+
+       // Filename of Kml file within zip archive
+       private static final String KML_FILENAME_IN_KMZ = "doc.kml";
+       // Width and height of thumbnail images in Kmz
+       private static final int THUMBNAIL_WIDTH = 240;
+       private static final int THUMBNAIL_HEIGHT = 180;
 
 
        /**
-        * Constructor giving App object, frame and track
-        * @param inApp application object to inform of success
+        * Constructor giving frame and track
         * @param inParentFrame parent frame
-        * @param inTrack track object to save
+        * @param inTrackInfo track info object to save
         */
-       public KmlExporter(App inApp, JFrame inParentFrame, Track inTrack)
+       public KmlExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
        {
-               _app = inApp;
                _parentFrame = inParentFrame;
-               _track = inTrack;
+               _trackInfo = inTrackInfo;
+               _track = inTrackInfo.getTrack();
        }
 
 
        /**
         * Show the dialog to select options and export file
         */
-       public boolean showDialog()
+       public void showDialog()
        {
-               boolean fileSaved = false;
-               Object description = JOptionPane.showInputDialog(_parentFrame,
-                       I18nManager.getText("dialog.exportkml.text"),
-                       I18nManager.getText("dialog.exportkml.title"),
-                       JOptionPane.QUESTION_MESSAGE, null, null, "");
-               // TODO: Make dialog window including colour selection, line width, track description
-               if (description != null)
+               // Make dialog window including whether to compress to kmz (and include pictures) or not
+               if (_dialog == null)
                {
-                       // OK pressed, so choose output file
-                       if (_fileChooser == null)
-                               _fileChooser = new JFileChooser();
-                       _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
-                       _fileChooser.setFileFilter(new FileFilter() {
-                               public boolean accept(File f)
-                               {
-                                       return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".kml")));
-                               }
-                               public String getDescription()
-                               {
-                                       return I18nManager.getText("dialog.exportkml.filetype");
-                               }
-                       });
-                       _fileChooser.setAcceptAllFileFilterUsed(false);
-                       // Allow choose again if an existing file is selected
-                       boolean chooseAgain = false;
-                       do
-                       {
-                               chooseAgain = false;
-                               if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
-                               {
-                                       // OK pressed and file chosen
-                                       File file = _fileChooser.getSelectedFile();
-                                       if (!file.getName().toLowerCase().endsWith(".kml"))
-                                       {
-                                               file = new File(file.getAbsolutePath() + ".kml");
-                                       }
-                                       // Check if file exists and if necessary prompt for overwrite
-                                       Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
-                                       if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
-                                                       I18nManager.getText("dialog.save.overwrite.text"),
-                                                       I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
-                                                       JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
-                                               == JOptionPane.YES_OPTION)
-                                       {
-                                               if (exportFile(file, description.toString()))
-                                               {
-                                                       fileSaved = true;
-                                               }
-                                               else
-                                               {
-                                                       chooseAgain = true;
-                                               }
-                                       }
-                                       else
-                                       {
-                                               chooseAgain = true;
-                                       }
-                               }
-                       } while (chooseAgain);
+                       _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportkml.title"), true);
+                       _dialog.setLocationRelativeTo(_parentFrame);
+                       _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+                       _dialog.getContentPane().add(makeDialogComponents());
+                       _dialog.pack();
                }
-               return fileSaved;
+               enableCheckboxes();
+               _progressBar.setVisible(false);
+               _dialog.show();
        }
 
 
        /**
-        * Export the track data to the specified file with description
-        * @param inFile File object to save to
-        * @param inDescription description to use, if any
+        * Create dialog components
+        * @return Panel containing all gui elements in dialog
         */
-       private boolean exportFile(File inFile, String inDescription)
+       private Component makeDialogComponents()
        {
-               FileWriter writer = null;
-               try
+               JPanel dialogPanel = new JPanel();
+               dialogPanel.setLayout(new BorderLayout());
+               JPanel mainPanel = new JPanel();
+               mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+               // Make a central panel with the text box and checkboxes
+               JPanel descPanel = new JPanel();
+               descPanel.setLayout(new FlowLayout());
+               descPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.text")));
+               _descriptionField = new JTextField(20);
+               descPanel.add(_descriptionField);
+               mainPanel.add(descPanel);
+               dialogPanel.add(mainPanel, BorderLayout.CENTER);
+               // Checkboxes for kmz export and image export
+               _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
+               _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
+               _kmzCheckbox.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // enable image checkbox if kmz activated
+                               enableCheckboxes();
+                       }
+               });
+               mainPanel.add(_kmzCheckbox);
+               _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
+               _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
+               mainPanel.add(_exportImagesCheckbox);
+               mainPanel.add(Box.createVerticalStrut(10));
+               _progressBar = new JProgressBar(0, 100);
+               _progressBar.setVisible(false);
+               mainPanel.add(_progressBar);
+               mainPanel.add(Box.createVerticalStrut(10));
+               // button panel at bottom
+               JPanel buttonPanel = new JPanel();
+               buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+               JButton okButton = new JButton(I18nManager.getText("button.ok"));
+               ActionListener okListener = new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               startExport();
+                       }
+               };
+               okButton.addActionListener(okListener);
+               _descriptionField.addActionListener(okListener);
+               buttonPanel.add(okButton);
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _dialog.dispose();
+                       }
+               });
+               buttonPanel.add(cancelButton);
+               dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
+               return dialogPanel;
+       }
+
+
+       /**
+        * Enable the checkboxes according to data
+        */
+       private void enableCheckboxes()
+       {
+               boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
+               _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
+               _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
+       }
+
+
+       /**
+        * Start the export process based on the input parameters
+        */
+       private void startExport()
+       {
+               // OK pressed, so choose output file
+               if (_fileChooser == null)
+                       {_fileChooser = new JFileChooser();}
+               _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
+               _fileChooser.setFileFilter(new FileFilter() {
+                       public boolean accept(File f)
+                       {
+                               return (f != null && (f.isDirectory()
+                                       || f.getName().toLowerCase().endsWith(".kml") || f.getName().toLowerCase().endsWith(".kmz")));
+                       }
+                       public String getDescription()
+                       {
+                               return I18nManager.getText("dialog.exportkml.filetype");
+                       }
+               });
+               String requiredExtension = null, otherExtension = null;
+               if (_kmzCheckbox.isSelected())
                {
-                       writer = new FileWriter(inFile);
-                       writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
-                       writer.write("\t<name>");
-                       writer.write(inDescription);
-                       writer.write("</name>\n");
-
-                       int i = 0;
-                       DataPoint point = null;
-                       boolean hasTrackpoints = false;
-                       // Loop over waypoints
-                       boolean writtenPhotoHeader = false;
-                       int numPoints = _track.getNumPoints();
-                       for (i=0; i<numPoints; i++)
+                       requiredExtension = ".kmz"; otherExtension = ".kml";
+               }
+               else
+               {
+                       requiredExtension = ".kml"; otherExtension = ".kmz";
+               }
+               _fileChooser.setAcceptAllFileFilterUsed(false);
+               // Allow choose again if an existing file is selected
+               boolean chooseAgain = false;
+               do
+               {
+                       chooseAgain = false;
+                       if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
                        {
-                               point = _track.getPoint(i);
-                               if (point.isWaypoint())
+                               // OK pressed and file chosen
+                               File file = _fileChooser.getSelectedFile();
+                               if (file.getName().toLowerCase().endsWith(otherExtension))
+                               {
+                                       String path = file.getAbsolutePath();
+                                       file = new File(path.substring(0, path.length()-otherExtension.length()) + requiredExtension);
+                               }
+                               else if (!file.getName().toLowerCase().endsWith(requiredExtension))
                                {
-                                       exportWaypoint(point, writer);
+                                       file = new File(file.getAbsolutePath() + requiredExtension);
                                }
-                               else if (point.getPhoto() != null)
+                               // Check if file exists and if necessary prompt for overwrite
+                               Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
+                               if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
+                                               I18nManager.getText("dialog.save.overwrite.text"),
+                                               I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
+                                               JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
+                                       == JOptionPane.YES_OPTION)
                                {
-                                       if (!writtenPhotoHeader)
-                                       {
-                                               writer.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
-                                               writtenPhotoHeader = true;
-                                       }
-                                       exportPhotoPoint(point, writer);
+                                       // New file or overwrite confirmed, so initiate export in separate thread
+                                       _exportFile = file;
+                                       new Thread(this).start();
                                }
                                else
                                {
-                                       hasTrackpoints = true;
+                                       chooseAgain = true;
                                }
                        }
-                       if (hasTrackpoints)
+               } while (chooseAgain);
+       }
+
+
+       /**
+        * Run method for controlling separate thread for exporting
+        */
+       public void run()
+       {
+               // Initialise progress bar
+               _progressBar.setVisible(true);
+               _progressBar.setValue(0);
+               boolean exportToKmz = _kmzCheckbox.isSelected();
+               boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
+               _progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
+               OutputStreamWriter writer = null;
+               ZipOutputStream zipOutputStream = null;
+               try
+               {
+                       // Select writer according to whether kmz requested or not
+                       if (!_kmzCheckbox.isSelected())
+                       {
+                               // normal writing to file
+                               writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
+                       }
+                       else
                        {
-                               writer.write("\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
-                                       + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
-                                       + "\t\t</Style>\n\t\t<LineString>\n\t\t\t<coordinates>");
-                               // Loop over track points
-                               for (i=0; i<numPoints; i++)
+                               // kmz requested - need zip output stream
+                               zipOutputStream = new ZipOutputStream(new FileOutputStream(_exportFile));
+                               writer = new OutputStreamWriter(zipOutputStream);
+                               // Make an entry in the zip file for the kml file
+                               ZipEntry kmlEntry = new ZipEntry(KML_FILENAME_IN_KMZ);
+                               zipOutputStream.putNextEntry(kmlEntry);
+                       }
+                       // write file
+                       int numPoints = exportData(writer, exportImages);
+                       // update progress bar
+                       _progressBar.setValue(1);
+
+                       // close zip entry if necessary
+                       if (zipOutputStream != null)
+                       {
+                               // Make sure all buffered data in writer is flushed
+                               writer.flush();
+                               // Close off this entry in the zip file
+                               zipOutputStream.closeEntry();
+                               // Export images into zip file too if requested
+                               if (exportImages)
                                {
-                                       point = _track.getPoint(i);
-                                       if (!point.isWaypoint())
-                                       {
-                                               exportTrackpoint(point, writer);
-                                       }
+                                       // Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
+                                       exportThumbnails(zipOutputStream);
                                }
-                               writer.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
                        }
-                       writer.write("</Folder>\n</kml>");
+
+                       // close file
                        writer.close();
                        JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
                                 + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2")
-                                + " " + inFile.getAbsolutePath(),
+                                + " " + _exportFile.getAbsolutePath(),
                                I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
-                       return true;
+                       // export successful so need to close dialog and return
+                       _dialog.dispose();
+                       return;
                }
                catch (IOException ioe)
                {
+                       // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
                        try {
                                if (writer != null) writer.close();
                        }
                        catch (IOException ioe2) {}
-                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(),
+                       JOptionPane.showMessageDialog(_parentFrame,
+                               I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
                                I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
                }
-               return false;
+               // if not returned already, export failed so need to recall the file selection
+               startExport();
+       }
+
+
+       /**
+        * Export the information to the given writer
+        * @param inWriter writer object
+        * @param inExportImages true if image thumbnails are to be referenced
+        * @return number of points written
+        */
+       private int exportData(OutputStreamWriter inWriter, boolean inExportImages)
+       throws IOException
+       {
+               // TODO: Look at segments of track, and split into separate lines in Kml if necessary
+               inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
+               inWriter.write("\t<name>");
+               if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
+               {
+                       inWriter.write(_descriptionField.getText());
+               }
+               else
+               {
+                       inWriter.write("Export from Prune");
+               }
+               inWriter.write("</name>\n");
+
+               int i = 0;
+               DataPoint point = null;
+               boolean hasTrackpoints = false;
+               // Loop over waypoints
+               boolean writtenPhotoHeader = false;
+               int numPoints = _track.getNumPoints();
+               int photoNum = 0;
+               for (i=0; i<numPoints; i++)
+               {
+                       point = _track.getPoint(i);
+                       // Make a blob for each waypoint
+                       if (point.isWaypoint())
+                       {
+                               exportWaypoint(point, inWriter);
+                       }
+                       // Make a blob with description for each photo
+                       if (point.getPhoto() != null)
+                       {
+                               if (!writtenPhotoHeader)
+                               {
+                                       inWriter.write("<Style id=\"camera_icon\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/pal4/icon46.png</href></Icon></IconStyle></Style>");
+                                       writtenPhotoHeader = true;
+                               }
+                               photoNum++;
+                               exportPhotoPoint(point, inWriter, inExportImages, photoNum);
+                       }
+                       else
+                       {
+                               hasTrackpoints = true;
+                       }
+               }
+               // Make a line for the track, if there is one
+               if (hasTrackpoints)
+               {
+                       inWriter.write("\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+                               + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+                               + "\t\t</Style>\n\t\t<LineString>\n\t\t\t<coordinates>");
+                       // Loop over track points
+                       for (i=0; i<numPoints; i++)
+                       {
+                               point = _track.getPoint(i);
+                               if (!point.isWaypoint())
+                               {
+                                       exportTrackpoint(point, inWriter);
+                               }
+                       }
+                       inWriter.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
+               }
+               inWriter.write("</Folder>\n</kml>");
+               return numPoints;
        }
 
 
@@ -214,14 +415,26 @@ public class KmlExporter
         * Export the specified photo into the file
         * @param inPoint data point including photo
         * @param inWriter writer object
+        * @param inImageLink flag to set whether to export image links or not
+        * @param inImageNumber number of image for filename
         * @throws IOException on write failure
         */
-       private void exportPhotoPoint(DataPoint inPoint, Writer inWriter) throws IOException
+       private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink, int inImageNumber)
+       throws IOException
        {
-               // TODO: Export photos to KML too - for photos need kmz!
                inWriter.write("\t<Placemark>\n\t\t<name>");
                inWriter.write(inPoint.getPhoto().getFile().getName());
                inWriter.write("</name>\n");
+               if (inImageLink)
+               {
+                       // Work out image dimensions of thumbnail
+                       Dimension picSize = inPoint.getPhoto().getSize();
+                       Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+                       // Write out some html for the thumbnail images
+                       inWriter.write("<description><![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
+                               + inImageNumber + ".jpg' width='" + thumbSize.width + "' height='" + thumbSize.height + "'></center></td></tr>"
+                               + "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
+               }
                inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
                inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
                inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
@@ -244,4 +457,72 @@ public class KmlExporter
                // Altitude not exported, locked to ground by Google Earth
                inWriter.write(",0\n");
        }
+
+
+       /**
+        * Loop through the photos and create thumbnails
+        * @param inZipStream zip stream to save image files to
+        */
+       private void exportThumbnails(ZipOutputStream inZipStream) throws IOException
+       {
+               // set up image writer
+               Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
+               if (writers == null || !writers.hasNext())
+               {
+                       throw new IOException("no JPEG writer found");
+               }
+               ImageWriter imageWriter = (ImageWriter) writers.next();
+
+               int numPoints = _track.getNumPoints();
+               DataPoint point = null;
+               int photoNum = 0;
+               // Loop over all points in track
+               for (int i=0; i<numPoints; i++)
+               {
+                       point = _track.getPoint(i);
+                       if (point.getPhoto() != null)
+                       {
+                               photoNum++;
+                               // Make a new entry in zip file
+                               ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
+                               inZipStream.putNextEntry(entry);
+                               // Load image and write to outstream
+                               ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
+
+                               // Scale and smooth image to required size
+                               Dimension outputSize = ImageUtils.getThumbnailSize(
+                                       point.getPhoto().getWidth(), point.getPhoto().getHeight(),
+                                       THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+                               BufferedImage bufferedImage = ImageUtils.createScaledImage(icon.getImage(), outputSize.width, outputSize.height);
+
+                               imageWriter.setOutput(ImageIO.createImageOutputStream(inZipStream));
+                               imageWriter.write(bufferedImage);
+                               // Close zip file entry
+                               inZipStream.closeEntry();
+                               // Update progress bar
+                               _progressBar.setValue(photoNum+1);
+                       }
+               }
+       }
+
+
+       /**
+        * @return number of correlated photos in the track
+        */
+       private int getNumPhotosToExport()
+       {
+               int numPoints = _track.getNumPoints();
+               int numPhotos = 0;
+               DataPoint point = null;
+               // Loop over all points in track
+               for (int i=0; i<numPoints; i++)
+               {
+                       point = _track.getPoint(i);
+                       if (point.getPhoto() != null)
+                       {
+                               numPhotos++;
+                       }
+               }
+               return numPhotos;
+       }
 }
diff --git a/tim/prune/save/PhotoTableEntry.java b/tim/prune/save/PhotoTableEntry.java
new file mode 100644 (file)
index 0000000..8c44f93
--- /dev/null
@@ -0,0 +1,98 @@
+package tim.prune.save;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoStatus;
+
+/**
+ * Class to represent a row of the photo table for saving exif
+ */
+public class PhotoTableEntry
+{
+       private Photo _photo = null;
+       private String _photoName = null;
+       private boolean _save = true;
+       private String _status = null;
+
+       /**
+        * Constructor
+        * @param inPhoto photo object
+        */
+       public PhotoTableEntry(Photo inPhoto)
+       {
+               _photo = inPhoto;
+               if (inPhoto != null)
+               {
+                       _photoName = inPhoto.getFile().getName();
+                       _status = getStatusString(inPhoto.getOriginalStatus(), inPhoto.getCurrentStatus());
+               }
+       }
+
+
+       /**
+        * Make a status string from the given status bytes
+        * @param inOriginalStatus original status of photo
+        * @param inCurrentStatus current status of photo
+        * @return status string for display
+        */
+       private static String getStatusString (byte inOriginalStatus, byte inCurrentStatus)
+       {
+               if (inOriginalStatus != inCurrentStatus)
+               {
+                       if (inOriginalStatus == PhotoStatus.NOT_CONNECTED)
+                       {
+                               // originally didn't have a point, now it has
+                               return I18nManager.getText("dialog.saveexif.photostatus.connected");
+                       }
+                       if (inCurrentStatus == PhotoStatus.NOT_CONNECTED)
+                       {
+                               // originally had a point, now it doesn't
+                               return I18nManager.getText("dialog.saveexif.photostatus.disconnected");
+                       }
+                       // originally had a point, now it has a different one
+                       return I18nManager.getText("dialog.saveexif.photostatus.modified");
+               }
+               // unrecognised status
+               return null;
+       }
+
+       /**
+        * @return Photo object
+        */
+       public Photo getPhoto()
+       {
+               return _photo;
+       }
+
+       /**
+        * @return photo filename
+        */
+       public String getName()
+       {
+               return _photoName;
+       }
+
+       /**
+        * @return photo status as string
+        */
+       public String getStatus()
+       {
+               return _status;
+       }
+
+       /**
+        * @param inFlag true to save exif, false otherwise
+        */
+       public void setSaveFlag(boolean inFlag)
+       {
+               _save = inFlag;
+       }
+
+       /**
+        * @return true to save exif, false otherwise
+        */
+       public boolean getSaveFlag()
+       {
+               return _save;
+       }
+}
diff --git a/tim/prune/save/PhotoTableModel.java b/tim/prune/save/PhotoTableModel.java
new file mode 100644 (file)
index 0000000..fd66590
--- /dev/null
@@ -0,0 +1,138 @@
+package tim.prune.save;
+
+import javax.swing.table.AbstractTableModel;
+
+import tim.prune.I18nManager;
+
+/**
+ * Class to hold table model information for save exif dialog
+ */
+public class PhotoTableModel extends AbstractTableModel
+{
+       private PhotoTableEntry[] _photos = null;
+       private int _addIndex = 0;
+
+
+       /**
+        * Constructor giving list size
+        */
+       public PhotoTableModel(int inSize)
+       {
+               _photos = new PhotoTableEntry[inSize];
+       }
+
+
+       /**
+        * Set the given PhotoTableEntry object in the array
+        * @param inEntry PhotoTableEntry object describing the photo
+        */
+       public void addPhotoInfo(PhotoTableEntry inEntry)
+       {
+               if (_addIndex < _photos.length && inEntry != null
+                       && inEntry.getStatus() != null)
+               {
+                       _photos[_addIndex] = inEntry;
+                       _addIndex++;
+               }
+       }
+
+       /**
+        * @return the number of photos in the list whose status has changed
+        */
+       public int getNumSaveablePhotos()
+       {
+               return _addIndex;
+       }
+
+       /**
+        * @see javax.swing.table.TableModel#getColumnCount()
+        */
+       public int getColumnCount()
+       {
+               return 3;
+       }
+
+
+       /**
+        * @see javax.swing.table.TableModel#getRowCount()
+        */
+       public int getRowCount()
+       {
+               return _addIndex;
+       }
+
+
+       /**
+        * @see javax.swing.table.TableModel#getValueAt(int, int)
+        */
+       public Object getValueAt(int inRowIndex, int inColumnIndex)
+       {
+               if (inColumnIndex == 0)
+               {
+                       return _photos[inRowIndex].getName();
+               }
+               else if (inColumnIndex == 1)
+               {
+                       return _photos[inRowIndex].getStatus();
+               }
+               return new Boolean(_photos[inRowIndex].getSaveFlag());
+       }
+
+
+       /**
+        * @return true if cell is editable
+        */
+       public boolean isCellEditable(int inRowIndex, int inColumnIndex)
+       {
+               // only the save column is editable
+               return inColumnIndex == 2;
+       }
+
+
+       /**
+        * Set the given cell value
+        * @see javax.swing.table.TableModel#setValueAt(java.lang.Object, int, int)
+        */
+       public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex)
+       {
+               // ignore edits to other columns
+               if (inColumnIndex == 2)
+                       _photos[inRowIndex].setSaveFlag(((Boolean) inValue).booleanValue());
+       }
+
+
+       /**
+        * @return Class of cell data
+        */
+       public Class getColumnClass(int inColumnIndex)
+       {
+               if (inColumnIndex < 2) return String.class;
+               return Boolean.class;
+       }
+
+
+       /**
+        * Get the name of the column
+        */
+       public String getColumnName(int inColNum)
+       {
+               if (inColNum == 0) return I18nManager.getText("dialog.saveexif.table.photoname");
+               else if (inColNum == 1) return I18nManager.getText("dialog.saveexif.table.status");
+               return I18nManager.getText("dialog.saveexif.table.save");
+       }
+
+
+       /**
+        * Retrieve the object at the given index
+        * @param inIndex index, starting at 0
+        * @return PhotoTableEntry object at this position
+        */
+       public PhotoTableEntry getPhotoTableEntry(int inIndex)
+       {
+               if (inIndex < 0 || inIndex >= _photos.length)
+               {
+                       return null;
+               }
+               return _photos[inIndex];
+       }
+}
index c1f90fd24667003a9f3fa29d9c51d5b0c923d076..3696dd10811986994943af9d7bdcc431a7f3eeb7 100644 (file)
@@ -21,9 +21,9 @@ import javax.swing.JTextField;
 import javax.swing.SwingConstants;
 import javax.swing.filechooser.FileFilter;
 
-import tim.prune.App;
 import tim.prune.I18nManager;
 import tim.prune.data.Track;
+import tim.prune.threedee.LineDialog;
 import tim.prune.threedee.ThreeDModel;
 
 /**
@@ -32,7 +32,6 @@ import tim.prune.threedee.ThreeDModel;
  */
 public class PovExporter
 {
-       private App _app = null;
        private JFrame _parentFrame = null;
        private Track _track = null;
        private JDialog _dialog = null;
@@ -49,14 +48,12 @@ public class PovExporter
 
 
        /**
-        * Constructor giving App object, frame and track
-        * @param inApp application object to inform of success
+        * Constructor giving frame and track
         * @param inParentFrame parent frame
         * @param inTrack track object to save
         */
-       public PovExporter(App inApp, JFrame inParentFrame, Track inTrack)
+       public PovExporter(JFrame inParentFrame, Track inTrack)
        {
-               _app = inApp;
                _parentFrame = inParentFrame;
                _track = inTrack;
                // Set default camera coordinates
@@ -188,6 +185,22 @@ public class PovExporter
 
                JPanel flowPanel = new JPanel();
                flowPanel.add(centralPanel);
+               
+               // show lines button
+               JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
+               showLinesButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // Need to scale model to find lines
+                               ThreeDModel model = new ThreeDModel(_track);
+                               model.scale();
+                               double[] latLines = model.getLatitudeLines();
+                               double[] lonLines = model.getLongitudeLines();
+                               LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
+                               dialog.showDialog();
+                       }
+               });
+               flowPanel.add(showLinesButton);
                panel.add(flowPanel, BorderLayout.CENTER);
                return panel;
        }
@@ -204,7 +217,6 @@ public class PovExporter
                _cameraZ = checkCoordinate(_cameraZField.getText());
 
                // OK pressed, so choose output file
-               boolean fileSaved = false;
                if (_fileChooser == null)
                        _fileChooser = new JFileChooser();
                _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
@@ -244,7 +256,7 @@ public class PovExporter
                                        // Export the file
                                        if (exportFile(file))
                                        {
-                                               fileSaved = true;
+                                               // file saved
                                        }
                                        else
                                        {
@@ -479,14 +491,14 @@ public class PovExporter
        {
                inWriter.write("// Latitude and longitude lines:");
                inWriter.write(inLineSeparator);
-               int numlines = inModel.getNumLatitudeLines();
+               int numlines = inModel.getLatitudeLines().length;
                for (int i=0; i<numlines; i++)
                {
                        // write cylinder to file
                        inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
                        inWriter.write(inLineSeparator);
                }
-               numlines = inModel.getNumLongitudeLines();
+               numlines = inModel.getLongitudeLines().length;
                for (int i=0; i<numlines; i++)
                {
                        // write cylinder to file
index e65d53de50c2f70f1aca7492e5489dad343f4e11..b6a017ae05e90606381cff9cc80385ab3ade0301 100644 (file)
@@ -1,7 +1,7 @@
 package tim.prune.threedee;
 
-import java.awt.BorderLayout;
 import java.awt.FlowLayout;
+import java.awt.BorderLayout;
 import java.awt.Font;
 import java.awt.GraphicsConfiguration;
 import java.awt.GraphicsEnvironment;
@@ -55,6 +55,7 @@ public class Java3DWindow implements ThreeDWindow
        private Track _track = null;
        private JFrame _parentFrame = null;
        private JFrame _frame = null;
+       private ThreeDModel _model = null;
        private OrbitBehavior _orbit = null;
        private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
 
@@ -64,7 +65,6 @@ public class Java3DWindow implements ThreeDWindow
        // Constants
        private static final double INITIAL_Y_ROTATION = -25.0;
        private static final double INITIAL_X_ROTATION = 15.0;
-       private static final int INITIAL_ALTITUDE_CAP = 500;
        private static final String CARDINALS_FONT = "Arial";
        private static final int MAX_TRACK_SIZE = 2500; // threshold for warning
 
@@ -185,6 +185,19 @@ public class Java3DWindow implements ThreeDWindow
                                }
                        }});
                panel.add(renderButton);
+               // Display coordinates of lat/long lines of 3d graph in separate dialog
+               JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
+               showLinesButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               double[] latLines = _model.getLatitudeLines();
+                               double[] lonLines = _model.getLongitudeLines();
+                               LineDialog dialog = new LineDialog(_frame, latLines, lonLines);
+                               dialog.showDialog();
+                       }
+               });
+               panel.add(showLinesButton);
+               // Close button
                JButton closeButton = new JButton(I18nManager.getText("button.close"));
                closeButton.addActionListener(new ActionListener()
                {
@@ -241,8 +254,6 @@ public class Java3DWindow implements ThreeDWindow
                // Base plane
                Appearance planeAppearance = null;
                Box plane = null;
-               Transform3D planeShift = null;
-               TransformGroup planeTrans = null;
                planeAppearance = new Appearance();
                planeAppearance.setMaterial(new Material(new Color3f(0.1f, 0.2f, 0.2f),
                 new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.3f, 0.4f, 0.4f),
@@ -268,15 +279,15 @@ public class Java3DWindow implements ThreeDWindow
                objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.e"), new Point3f(10f, 0f, 0f), compassFont));
 
                // create and scale model
-               ThreeDModel model = new ThreeDModel(_track);
-               model.setAltitudeCap(_altitudeCap);
-               model.scale();
+               _model = new ThreeDModel(_track);
+               _model.setAltitudeCap(_altitudeCap);
+               _model.scale();
 
                // Lat/Long lines
-               objTrans.addChild(createLatLongs(model));
+               objTrans.addChild(createLatLongs(_model));
 
                // Add points to model
-               objTrans.addChild(createDataPoints(model));
+               objTrans.addChild(createDataPoints(_model));
 
                // Create lights
                BoundingSphere bounds =
@@ -339,12 +350,12 @@ public class Java3DWindow implements ThreeDWindow
        private static Group createLatLongs(ThreeDModel inModel)
        {
                Group group = new Group();
-               int numlines = inModel.getNumLatitudeLines();
+               int numlines = inModel.getLatitudeLines().length;
                for (int i=0; i<numlines; i++)
                {
                        group.addChild(createLatLine(inModel.getScaledLatitudeLine(i), inModel.getModelSize()));
                }
-               numlines = inModel.getNumLongitudeLines();
+               numlines = inModel.getLongitudeLines().length;
                for (int i=0; i<numlines; i++)
                {
                        group.addChild(createLonLine(inModel.getScaledLongitudeLine(i), inModel.getModelSize()));
diff --git a/tim/prune/threedee/LineDialog.java b/tim/prune/threedee/LineDialog.java
new file mode 100644 (file)
index 0000000..a11525f
--- /dev/null
@@ -0,0 +1,111 @@
+package tim.prune.threedee;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JEditorPane;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Latitude;
+import tim.prune.data.Longitude;
+
+/**
+ * Class to show a dialog displaying the line coordinates
+ * for a 3d view (either java3d or povray)
+ */
+public class LineDialog
+{
+       private JDialog _dialog = null;
+       private JFrame _parent = null;
+       private double[] _latLines = null;
+       private double[] _lonLines = null;
+
+
+       /**
+        * Constructor giving parent frame, latitude and longitude lines
+        * @param inParent parent frame for dialog
+        * @param inLatLines latitude lines as doubles
+        * @param inLonLines longitude lines as doubles
+        */
+       public LineDialog(JFrame inParent, double[] inLatLines, double[] inLonLines)
+       {
+               _parent = inParent;
+               _latLines = inLatLines;
+               _lonLines = inLonLines;
+       }
+
+
+       /**
+        * Show the dialog with the lines
+        */
+       public void showDialog()
+       {
+               _dialog = new JDialog(_parent, I18nManager.getText("dialog.3dlines.title"), true);
+               _dialog.setLocationRelativeTo(_parent);
+               _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+               _dialog.getContentPane().add(makeDialogComponents());
+               _dialog.pack();
+               _dialog.show();
+       }
+
+
+       /**
+        * @return dialog components
+        */
+       private JPanel makeDialogComponents()
+       {
+               JPanel panel = new JPanel();
+               panel.setLayout(new BorderLayout());
+               StringBuffer descBuffer = new StringBuffer();
+               if (_latLines == null || _latLines.length == 0 || _lonLines == null || _lonLines.length == 0)
+               {
+                       descBuffer.append("<p>").append(I18nManager.getText("dialog.3dlines.empty")).append("</p>");
+               }
+               else
+               {
+                       descBuffer.append("<p>").append(I18nManager.getText("dialog.3dlines.intro")).append(":</p>");
+                       descBuffer.append("<p>").append(I18nManager.getText("fieldname.latitude")).append("<ul>");
+                       Latitude lat = null;
+                       for (int i=0; i<_latLines.length; i++)
+                       {
+                               lat = new Latitude(_latLines[i], Latitude.FORMAT_DEG);
+                               descBuffer.append("<li>").append(lat.output(Latitude.FORMAT_DEG_WHOLE_MIN)).append("</li>");
+                       }
+                       descBuffer.append("</ul></p>");
+                       descBuffer.append("<p>").append(I18nManager.getText("fieldname.longitude")).append("<ul>");
+                       Longitude lon = null;
+                       for (int i=0; i<_lonLines.length; i++)
+                       {
+                               lon = new Longitude(_lonLines[i], Longitude.FORMAT_DEG);
+                               descBuffer.append("<li>").append(lon.output(Longitude.FORMAT_DEG_WHOLE_MIN)).append("</li>");
+                       }
+                       descBuffer.append("</ul></p>");
+               }
+               JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString());
+               descPane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
+               descPane.setEditable(false);
+               descPane.setOpaque(false);
+               panel.add(descPane, BorderLayout.CENTER);
+               // ok button
+               JPanel buttonPanel = new JPanel();
+               buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+               JButton okButton = new JButton(I18nManager.getText("button.ok"));
+               okButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _dialog.dispose();
+                               _dialog = null;
+                       }
+               });
+               buttonPanel.add(okButton);
+               panel.add(buttonPanel, BorderLayout.SOUTH);
+               return panel;
+       }
+}
index db748c0bd825ccc917c87a6664ca94781c40196a..141ec38a88d8656e18110e68181bb238b99f170c 100644 (file)
@@ -178,11 +178,11 @@ public class ThreeDModel
 
 
        /**
-        * @return number of latitude lines
+        * @return latitude lines
         */
-       public int getNumLatitudeLines()
+       public double[] getLatitudeLines()
        {
-               return _scaler.getLatitudeLines().length;
+               return _scaler.getLatitudeLines();
        }
 
        /**
@@ -195,11 +195,11 @@ public class ThreeDModel
        }
 
        /**
-        * @return number of longitude lines
+        * @return longitude lines
         */
-       public int getNumLongitudeLines()
+       public double[] getLongitudeLines()
        {
-               return _scaler.getLongitudeLines().length;
+               return _scaler.getLongitudeLines();
        }
 
        /**
index 89560efa1b2b7e592c8c7dfa2412bc8bf4b72a23..5a926b76f5d569156acb729b1aca3a25ae94241c 100644 (file)
@@ -31,13 +31,13 @@ public abstract class WindowFactory
        /**
         * @return true if 3d capability is installed
         */
-       private static boolean isJava3dEnabled()
+       public static boolean isJava3dEnabled()
        {
                boolean has3d = false;
                try
                {
                        Class universeClass = Class.forName("com.sun.j3d.utils.universe.SimpleUniverse");
-                       has3d = true;
+                       has3d = (universeClass != null);
                }
                catch (ClassNotFoundException e)
                {
diff --git a/tim/prune/undo/UndoConnectPhoto.java b/tim/prune/undo/UndoConnectPhoto.java
new file mode 100644 (file)
index 0000000..99e6432
--- /dev/null
@@ -0,0 +1,58 @@
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Photo;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo the connection of a photo to a point\r
+ */\r
+public class UndoConnectPhoto implements UndoOperation\r
+{\r
+       private DataPoint _point = null;\r
+       private String _filename = null;\r
+\r
+\r
+       /**\r
+        * Constructor\r
+        * @param inPoint data point\r
+        * @param inFilename filename of photo\r
+        */\r
+       public UndoConnectPhoto(DataPoint inPoint, String inFilename)\r
+       {\r
+               _point = inPoint;\r
+               _filename = inFilename;\r
+       }\r
+\r
+\r
+       /**\r
+        * @return description of operation including photo filename\r
+        */\r
+       public String getDescription()\r
+       {\r
+               String desc = I18nManager.getText("undo.connectphoto") + " " + _filename;\r
+               return desc;\r
+       }\r
+\r
+\r
+       /**\r
+        * Perform the undo operation on the given Track\r
+        * @param inTrackInfo TrackInfo object on which to perform the operation\r
+        */\r
+       public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+       {\r
+               // Disconnect again\r
+               Photo photo = _point.getPhoto();\r
+               if (photo != null)\r
+               {\r
+                       _point.setPhoto(null);\r
+                       photo.setDataPoint(null);\r
+               }\r
+               else\r
+               {\r
+                       // throw exception if failed\r
+                       throw new UndoException(getDescription());\r
+               }\r
+       }\r
+}
\ No newline at end of file
diff --git a/tim/prune/undo/UndoDeletePhoto.java b/tim/prune/undo/UndoDeletePhoto.java
new file mode 100644 (file)
index 0000000..cfc1662
--- /dev/null
@@ -0,0 +1,73 @@
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Photo;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a delete of a single photo, either with or without point\r
+ */\r
+public class UndoDeletePhoto implements UndoOperation\r
+{\r
+       private int _photoIndex = -1;\r
+       private Photo _photo = null;\r
+       private int _pointIndex = -1;\r
+       private DataPoint _point = null;\r
+\r
+\r
+       /**\r
+        * Constructor\r
+        * @param inPhoto photo\r
+        * @param inPhotoIndex index number of photo within photo list\r
+        * @param inPoint data point\r
+        * @param inPointIndex index number of point within track\r
+        */\r
+       public UndoDeletePhoto(Photo inPhoto, int inPhotoIndex, DataPoint inPoint, int inPointIndex)\r
+       {\r
+               _photo = inPhoto;\r
+               _photoIndex = inPhotoIndex;\r
+               _point = inPoint;\r
+               _pointIndex = inPointIndex;\r
+       }\r
+\r
+\r
+       /**\r
+        * @return description of operation including photo name\r
+        */\r
+       public String getDescription()\r
+       {\r
+               String desc = I18nManager.getText("undo.deletephoto") + " " + _photo.getFile().getName();\r
+               return desc;\r
+       }\r
+\r
+\r
+       /**\r
+        * Perform the undo operation on the given Track\r
+        * @param inTrackInfo TrackInfo object on which to perform the operation\r
+        */\r
+       public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+       {\r
+               // restore photo\r
+               inTrackInfo.getPhotoList().addPhoto(_photo, _photoIndex);\r
+               // if there's a point to restore, restore it\r
+               if (_point != null)\r
+               {\r
+                       if (!inTrackInfo.getTrack().insertPoint(_point, _pointIndex))\r
+                       {\r
+                               throw new UndoException(getDescription());\r
+                       }\r
+               }\r
+               else\r
+               {\r
+                       // update needed if not already triggered by track update\r
+                       inTrackInfo.triggerUpdate();\r
+               }\r
+               // Ensure that photo is associated with point and vice versa\r
+               _photo.setDataPoint(_point);\r
+               if (_point != null)\r
+               {\r
+                       _point.setPhoto(_photo);\r
+               }\r
+       }\r
+}\r
index 4cc09b47f23aa752b1b362f515d1c27acb562bf5..daab9fd574c89d177737f9adaf2a34636d09e6bf 100644 (file)
@@ -11,17 +11,20 @@ public class UndoDeletePoint implements UndoOperation
 {\r
        private int _pointIndex = -1;\r
        private DataPoint _point = null;\r
+       private int _photoIndex = -1;\r
 \r
 \r
        /**\r
         * Constructor\r
-        * @param inIndex index number of point within track\r
+        * @param inPointIndex index number of point within track\r
         * @param inPoint data point\r
+        * @param inPhotoIndex index number of photo within photo list\r
         */\r
-       public UndoDeletePoint(int inIndex, DataPoint inPoint)\r
+       public UndoDeletePoint(int inPointIndex, DataPoint inPoint, int inPhotoIndex)\r
        {\r
-               _pointIndex = inIndex;\r
+               _pointIndex = inPointIndex;\r
                _point = inPoint;\r
+               _photoIndex = inPhotoIndex;\r
        }\r
 \r
 \r
@@ -40,7 +43,7 @@ public class UndoDeletePoint implements UndoOperation
 \r
        /**\r
         * Perform the undo operation on the given Track\r
-        * @param inTrack Track object on which to perform the operation\r
+        * @param inTrackInfo TrackInfo object on which to perform the operation\r
         */\r
        public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
        {\r
@@ -49,6 +52,17 @@ public class UndoDeletePoint implements UndoOperation
                {\r
                        throw new UndoException(getDescription());\r
                }\r
-               // TODO: Reinsert photo into list if necessary\r
+               // Re-attach / Re-insert photo into list if necessary\r
+               if (_point.getPhoto() != null && _photoIndex > -1)\r
+               {\r
+                       // Check if photo is still in list\r
+                       if (!inTrackInfo.getPhotoList().contains(_point.getPhoto()))\r
+                       {\r
+                               // photo has been removed - need to reinsert\r
+                               inTrackInfo.getPhotoList().addPhoto(_point.getPhoto(), _photoIndex);\r
+                       }\r
+                       // Ensure that photo is associated with point\r
+                       _point.getPhoto().setDataPoint(_point);\r
+               }\r
        }\r
-}
\ No newline at end of file
+}\r
index 6bb0b7df08aebc795a9ded8de37871051f71940f..4e526cd8193d99b5c73154d3326ff2c2a0f6b904 100644 (file)
@@ -2,6 +2,7 @@ package tim.prune.undo;
 \r
 import tim.prune.I18nManager;\r
 import tim.prune.data.DataPoint;\r
+import tim.prune.data.PhotoList;\r
 import tim.prune.data.TrackInfo;\r
 \r
 /**\r
@@ -11,6 +12,7 @@ public class UndoDeleteRange implements UndoOperation
 {\r
        private int _startIndex = -1;\r
        private DataPoint[] _points = null;\r
+       private PhotoList _photoList = null;\r
 \r
 \r
        /**\r
@@ -22,6 +24,7 @@ public class UndoDeleteRange implements UndoOperation
        {\r
                _startIndex = inTrackInfo.getSelection().getStart();\r
                _points = inTrackInfo.cloneSelectedRange();\r
+               _photoList = inTrackInfo.getPhotoList().cloneList();\r
        }\r
 \r
 \r
@@ -41,6 +44,17 @@ public class UndoDeleteRange implements UndoOperation
         */\r
        public void performUndo(TrackInfo inTrackInfo)\r
        {\r
+               // restore photos to how they were before\r
+               inTrackInfo.getPhotoList().restore(_photoList);\r
+               // reconnect photos to points\r
+               for (int i=0; i<_points.length; i++)\r
+               {\r
+                       DataPoint point = _points[i];\r
+                       if (point != null && point.getPhoto() != null)\r
+                       {\r
+                               point.getPhoto().setDataPoint(point);\r
+                       }\r
+               }\r
                // restore point array into track\r
                inTrackInfo.getTrack().insertRange(_points, _startIndex);\r
        }\r
index ae368652be0b36b98671c35dbcf213eb5f61c80c..a9f566531a37c3efd11ede111846dee844efedb1 100644 (file)
@@ -41,7 +41,7 @@ public class UndoEditPoint implements UndoOperation
 \r
        /**\r
         * Perform the undo operation on the given Track\r
-        * @param inTrack Track object on which to perform the operation\r
+        * @param inTrackInfo TrackInfo object on which to perform the operation\r
         */\r
        public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
        {\r
@@ -51,6 +51,5 @@ public class UndoEditPoint implements UndoOperation
                        // throw exception if failed\r
                        throw new UndoException(getDescription());\r
                }\r
-               // TODO: Deal with photo if necessary\r
        }\r
 }
\ No newline at end of file
index 8bf22183c5162c787fa8c7350f602a1da2c5f4df..d354b9dd8fb7a0e754d86fce62e12e30c1b4dc98 100644 (file)
@@ -2,6 +2,7 @@ package tim.prune.undo;
 \r
 import tim.prune.I18nManager;\r
 import tim.prune.data.DataPoint;\r
+import tim.prune.data.PhotoList;\r
 import tim.prune.data.TrackInfo;\r
 \r
 /**\r
@@ -13,6 +14,7 @@ public class UndoLoad implements UndoOperation
        private int _numLoaded = -1;\r
        private DataPoint[] _contents = null;\r
        private String _previousFilename = null;\r
+       private PhotoList _photoList = null;\r
 \r
 \r
        /**\r
@@ -33,14 +35,16 @@ public class UndoLoad implements UndoOperation
         * Constructor for replacing\r
         * @param inOldTrack track being replaced\r
         * @param inNumLoaded number of points loaded\r
+        * @param inPhotoList photo list, if any\r
         */\r
-       public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded)\r
+       public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded, PhotoList inPhotoList)\r
        {\r
                _cropIndex = -1;\r
                _numLoaded = inNumLoaded;\r
                _contents = inOldTrackInfo.getTrack().cloneContents();\r
                if (inOldTrackInfo.getFileInfo().getNumFiles() == 1)\r
                        _previousFilename = inOldTrackInfo.getFileInfo().getFilename();\r
+               _photoList = inPhotoList;\r
        }\r
 \r
 \r
@@ -76,6 +80,11 @@ public class UndoLoad implements UndoOperation
                }\r
                else\r
                {\r
+                       // replace photos how they were\r
+                       if (_photoList != null)\r
+                       {\r
+                               inTrackInfo.getPhotoList().restore(_photoList);\r
+                       }\r
                        // replace track contents with old\r
                        if (!inTrackInfo.getTrack().replaceContents(_contents))\r
                        {\r
index c08532d9ab9e01f9b4b819b5ccca88cf90f016ff..bb6a21176c48673f3a293115a97e6969fd98255e 100644 (file)
@@ -8,17 +8,19 @@ import tim.prune.data.TrackInfo;
  */\r
 public class UndoLoadPhotos implements UndoOperation\r
 {\r
-       private int _numLoaded = -1;\r
+       private int _numPhotos = -1;\r
+       private int _numPoints = -1;\r
 \r
-       // TODO: Handle possibility of photos not having datapoints (yet)\r
 \r
        /**\r
         * Constructor\r
-        * @param inNumLoaded number of photos loaded\r
+        * @param inNumPhotos number of photos loaded\r
+        * @param inNumPoints number of points loaded\r
         */\r
-       public UndoLoadPhotos(int inNumLoaded)\r
+       public UndoLoadPhotos(int inNumPhotos, int inNumPoints)\r
        {\r
-               _numLoaded = inNumLoaded;\r
+               _numPhotos = inNumPhotos;\r
+               _numPoints = inNumPoints;\r
        }\r
 \r
 \r
@@ -28,8 +30,8 @@ public class UndoLoadPhotos implements UndoOperation
        public String getDescription()\r
        {\r
                String desc = I18nManager.getText("undo.loadphotos");\r
-               if (_numLoaded > 0)\r
-                       desc = desc + " (" + _numLoaded + ")";\r
+               if (_numPhotos > 0)\r
+                       desc = desc + " (" + _numPhotos + ")";\r
                return desc;\r
        }\r
 \r
@@ -41,12 +43,15 @@ public class UndoLoadPhotos implements UndoOperation
         */\r
        public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
        {\r
+               int cropIndex;\r
                // crop track to previous size\r
-               int cropIndex = inTrackInfo.getTrack().getNumPoints() - _numLoaded;\r
-               inTrackInfo.getTrack().cropTo(cropIndex);\r
+               if (_numPoints > 0)\r
+               {\r
+                       cropIndex = inTrackInfo.getTrack().getNumPoints() - _numPoints;\r
+                       inTrackInfo.getTrack().cropTo(cropIndex);\r
+               }\r
                // crop photo list to previous size\r
-               // (currently it is assumed that the number of points is the same as number of photos)\r
-               cropIndex = inTrackInfo.getPhotoList().getNumPhotos() - _numLoaded;\r
+               cropIndex = inTrackInfo.getPhotoList().getNumPhotos() - _numPhotos;\r
                inTrackInfo.getPhotoList().cropTo(cropIndex);\r
                // clear selection\r
                inTrackInfo.getSelection().clearAll();\r