]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Version 4, January 2008
authoractivityworkshop <mail@activityworkshop.net>
Sat, 14 Feb 2015 13:51:17 +0000 (14:51 +0100)
committeractivityworkshop <mail@activityworkshop.net>
Sat, 14 Feb 2015 13:51:17 +0000 (14:51 +0100)
75 files changed:
tim/prune/App.java
tim/prune/DataSubscriber.java
tim/prune/GpsPruner.java
tim/prune/I18nManager.java
tim/prune/correlate/OptionsChangedListener.java [new file with mode: 0644]
tim/prune/correlate/PhotoCorrelator.java [new file with mode: 0644]
tim/prune/correlate/PhotoPreviewTableModel.java [new file with mode: 0644]
tim/prune/correlate/PhotoPreviewTableRow.java [new file with mode: 0644]
tim/prune/correlate/PhotoSelectionTableModel.java [new file with mode: 0644]
tim/prune/correlate/PhotoSelectionTableRow.java [new file with mode: 0644]
tim/prune/correlate/PointPair.java [new file with mode: 0644]
tim/prune/correlate/TimeIndexPair.java [new file with mode: 0644]
tim/prune/data/Altitude.java
tim/prune/data/AltitudeRange.java
tim/prune/data/Coordinate.java
tim/prune/data/DataPoint.java
tim/prune/data/Distance.java
tim/prune/data/Field.java
tim/prune/data/FieldList.java
tim/prune/data/FieldType.java
tim/prune/data/Latitude.java
tim/prune/data/Longitude.java
tim/prune/data/Photo.java
tim/prune/data/Selection.java
tim/prune/data/TimeDifference.java [new file with mode: 0644]
tim/prune/data/Timestamp.java
tim/prune/data/Track.java
tim/prune/data/TrackInfo.java
tim/prune/drew/jpeg/ExifReader.java
tim/prune/drew/jpeg/JpegData.java
tim/prune/drew/jpeg/JpegSegmentData.java
tim/prune/drew/jpeg/Rational.java
tim/prune/edit/EditFieldsTableModel.java
tim/prune/gui/AboutScreen.java
tim/prune/gui/DetailsDisplay.java
tim/prune/gui/MapChart.java
tim/prune/gui/MenuManager.java
tim/prune/gui/PhotoThumbnail.java
tim/prune/gui/ProfileChart.java
tim/prune/gui/SelectorDisplay.java
tim/prune/gui/UndoManager.java
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
tim/prune/lang/prune-texts_pl.properties [new file with mode: 0644]
tim/prune/load/DelimiterInfo.java
tim/prune/load/FieldGuesser.java [new file with mode: 0644]
tim/prune/load/FieldSelectionTableModel.java
tim/prune/load/FileCacher.java
tim/prune/load/FileSplitter.java
tim/prune/load/JpegLoader.java
tim/prune/load/PhotoMeasurer.java [deleted file]
tim/prune/load/PhotoSorter.java [new file with mode: 0644]
tim/prune/load/TextFileLoader.java
tim/prune/load/xml/GpxHandler.java
tim/prune/load/xml/XmlFileLoader.java
tim/prune/readme.txt
tim/prune/save/ExifSaver.java
tim/prune/save/FieldInfo.java
tim/prune/save/FieldSelectionTableModel.java
tim/prune/save/FileSaver.java
tim/prune/save/GpxExporter.java [new file with mode: 0644]
tim/prune/save/KmlExporter.java
tim/prune/save/PhotoTableModel.java
tim/prune/save/PovExporter.java
tim/prune/save/UpDownToggler.java
tim/prune/threedee/ThreeDWindow.java
tim/prune/undo/UndoConnectPhoto.java
tim/prune/undo/UndoCorrelatePhotos.java [new file with mode: 0644]
tim/prune/undo/UndoDeleteRange.java
tim/prune/undo/UndoDisconnectPhoto.java [new file with mode: 0644]
tim/prune/undo/UndoLoad.java
tim/prune/undo/UndoOperation.java

index e403722217d51bbe889168d7e66b03b3496899f8..18a2e79a60fa737cde270045cb0d3b6263dc278f 100644 (file)
@@ -1,12 +1,14 @@
 package tim.prune;
 
 import java.util.EmptyStackException;
-import java.util.List;
+import java.util.Set;
 import java.util.Stack;
 
 import javax.swing.JFrame;
 import javax.swing.JOptionPane;
 
+import tim.prune.correlate.PhotoCorrelator;
+import tim.prune.correlate.PointPair;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
 import tim.prune.data.Photo;
@@ -20,9 +22,9 @@ 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.GpxExporter;
 import tim.prune.save.KmlExporter;
 import tim.prune.save.PovExporter;
 import tim.prune.threedee.ThreeDException;
@@ -30,10 +32,12 @@ import tim.prune.threedee.ThreeDWindow;
 import tim.prune.threedee.WindowFactory;
 import tim.prune.undo.UndoCompress;
 import tim.prune.undo.UndoConnectPhoto;
+import tim.prune.undo.UndoCorrelatePhotos;
 import tim.prune.undo.UndoDeleteDuplicates;
 import tim.prune.undo.UndoDeletePhoto;
 import tim.prune.undo.UndoDeletePoint;
 import tim.prune.undo.UndoDeleteRange;
+import tim.prune.undo.UndoDisconnectPhoto;
 import tim.prune.undo.UndoEditPoint;
 import tim.prune.undo.UndoException;
 import tim.prune.undo.UndoInsert;
@@ -57,7 +61,9 @@ public class App
        private MenuManager _menuManager = null;
        private FileLoader _fileLoader = null;
        private JpegLoader _jpegLoader = null;
-       private KmlExporter _exporter = null;
+       private FileSaver _fileSaver = null;
+       private KmlExporter _kmlExporter = null;
+       private GpxExporter _gpxExporter = null;
        private PovExporter _povExporter = null;
        private Stack _undoStack = null;
        private UpdateMessageBroker _broker = null;
@@ -154,8 +160,10 @@ public class App
                }
                else
                {
-                       FileSaver saver = new FileSaver(this, _frame, _track);
-                       saver.showDialog(_fileLoader.getLastUsedDelimiter());
+                       if (_fileSaver == null) {
+                               _fileSaver = new FileSaver(this, _frame, _track);
+                       }
+                       _fileSaver.showDialog(_fileLoader.getLastUsedDelimiter());
                }
        }
 
@@ -173,11 +181,33 @@ public class App
                else
                {
                        // Invoke the export
-                       if (_exporter == null)
+                       if (_kmlExporter == null)
                        {
-                               _exporter = new KmlExporter(_frame, _trackInfo);
+                               _kmlExporter = new KmlExporter(_frame, _trackInfo);
                        }
-                       _exporter.showDialog();
+                       _kmlExporter.showDialog();
+               }
+       }
+
+
+       /**
+        * Export track data as Gpx
+        */
+       public void exportGpx()
+       {
+               if (_track == null)
+               {
+                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
+                               I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+               }
+               else
+               {
+                       // Invoke the export
+                       if (_gpxExporter == null)
+                       {
+                               _gpxExporter = new GpxExporter(_frame, _trackInfo);
+                       }
+                       _gpxExporter.showDialog();
                }
        }
 
@@ -208,6 +238,7 @@ public class App
         * @param inX X component of unit vector
         * @param inY Y component of unit vector
         * @param inZ Z component of unit vector
+        * @param inAltitudeCap altitude cap
         */
        private void exportPov(boolean inDefineSettings, double inX, double inY, double inZ, int inAltitudeCap)
        {
@@ -277,7 +308,8 @@ public class App
 
        /**
         * Complete the point edit
-        * @param inEditList list of edits
+        * @param inEditList field values to edit
+        * @param inUndoList field values before edit
         */
        public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
        {
@@ -570,6 +602,7 @@ public class App
 
        /**
         * Rearrange the waypoints into track order
+        * @param inFunction nearest point, all to end or all to start
         */
        public void rearrangeWaypoints(int inFunction)
        {
@@ -647,6 +680,8 @@ public class App
         * Receive loaded data and optionally merge with current Track
         * @param inFieldArray array of fields
         * @param inDataArray array of data
+        * @param inAltFormat altitude format
+        * @param inFilename filename used
         */
        public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
        {
@@ -695,6 +730,7 @@ public class App
                                _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
                                _lastSavePosition = _undoStack.size();
                                // TODO: Should be possible to reuse the Track object already loaded?
+                               _trackInfo.selectPoint(null);
                                _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
                                _trackInfo.getFileInfo().setFile(inFilename);
                                if (photos != null)
@@ -719,21 +755,19 @@ public class App
 
        /**
         * Accept a list of loaded photos
-        * @param inPhotoList List of Photo objects
+        * @param inPhotoSet Set of Photo objects
         */
-       public void informPhotosLoaded(List inPhotoList)
+       public void informPhotosLoaded(Set inPhotoSet)
        {
-               if (inPhotoList != null && !inPhotoList.isEmpty())
+               if (inPhotoSet != null && !inPhotoSet.isEmpty())
                {
-                       int[] numsAdded = _trackInfo.addPhotos(inPhotoList);
+                       int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
                        int numPhotosAdded = numsAdded[0];
                        int numPointsAdded = numsAdded[1];
                        if (numPhotosAdded > 0)
                        {
                                // 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 (numPhotosAdded == 1)
                        {
@@ -768,7 +802,25 @@ public class App
                        _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
                        photo.setDataPoint(point);
                        point.setPhoto(photo);
-                       //TODO: Confirm connect (maybe with status in photo panel?)
+                       _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+               }
+       }
+
+
+       /**
+        * Disconnect the current photo from its point
+        */
+       public void disconnectPhotoFromPoint()
+       {
+               Photo photo = _trackInfo.getCurrentPhoto();
+               if (photo != null && photo.getDataPoint() != null)
+               {
+                       DataPoint point = photo.getDataPoint();
+                       _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
+                       // disconnect
+                       photo.setDataPoint(null);
+                       point.setPhoto(null);
+                       _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
                }
        }
 
@@ -817,6 +869,117 @@ public class App
        }
 
 
+       /**
+        * Begin the photo correlation process by invoking dialog
+        */
+       public void beginCorrelatePhotos()
+       {
+               PhotoCorrelator correlator = new PhotoCorrelator(this, _frame);
+               // TODO: Do we need to keep a reference to this object to reuse it later?
+               correlator.begin();
+       }
+
+
+       /**
+        * Finish the photo correlation process
+        * @param inPointPairs array of PointPair objects describing operation
+        */
+       public void finishCorrelatePhotos(PointPair[] inPointPairs)
+       {
+               // TODO: This method is too big for App, but where should it go?
+               if (inPointPairs != null && inPointPairs.length > 0)
+               {
+                       // begin to construct undo information
+                       UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_trackInfo);
+                       // loop over Photos
+                       int arraySize = inPointPairs.length;
+                       int i = 0, numPhotos = 0;
+                       int numPointsToCreate = 0;
+                       PointPair pair = null;
+                       for (i=0; i<arraySize; i++)
+                       {
+                               pair = inPointPairs[i];
+                               if (pair != null && pair.isValid())
+                               {
+                                       if (pair.getMinSeconds() == 0L)
+                                       {
+                                               // exact match
+                                               Photo pointPhoto = pair.getPointBefore().getPhoto();
+                                               if (pointPhoto == null)
+                                               {
+                                                       // photo coincides with photoless point so connect the two
+                                                       pair.getPointBefore().setPhoto(pair.getPhoto());
+                                                       pair.getPhoto().setDataPoint(pair.getPointBefore());
+                                               }
+                                               else if (pointPhoto.equals(pair.getPhoto()))
+                                               {
+                                                       // photo is already connected, nothing to do
+                                               }
+                                               else
+                                               {
+                                                       // point is already connected to a different photo, so need to clone point
+                                                       numPointsToCreate++;
+                                               }
+                                       }
+                                       else
+                                       {
+                                               // photo time falls between two points, so need to interpolate new one
+                                               numPointsToCreate++;
+                                       }
+                                       numPhotos++;
+                               }
+                       }
+                       // Second loop, to create points if necessary
+                       if (numPointsToCreate > 0)
+                       {
+                               // make new array for added points
+                               DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
+                               int pointNum = 0;
+                               DataPoint pointToAdd = null;
+                               for (i=0; i<arraySize; i++)
+                               {
+                                       pair = inPointPairs[i];
+                                       if (pair != null && pair.isValid())
+                                       {
+                                               pointToAdd = null;
+                                               if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
+                                                && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
+                                               {
+                                                       // clone point
+                                                       pointToAdd = pair.getPointBefore().clonePoint();
+                                               }
+                                               else if (pair.getMinSeconds() > 0L)
+                                               {
+                                                       // interpolate point
+                                                       pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
+                                               }
+                                               if (pointToAdd != null)
+                                               {
+                                                       // link photo to point
+                                                       pointToAdd.setPhoto(pair.getPhoto());
+                                                       pair.getPhoto().setDataPoint(pointToAdd);
+                                                       // add to point array
+                                                       addedPoints[pointNum] = pointToAdd;
+                                                       pointNum++;
+                                               }
+                                       }
+                               }
+                               // expand track
+                               _track.appendPoints(addedPoints);
+                       }
+                       // add undo information to stack
+                       undo.setNumPhotosCorrelated(numPhotos);
+                       _undoStack.add(undo);
+                       // confirm correlation
+                       JOptionPane.showMessageDialog(_frame, "" + numPhotos + " "
+                                + (numPhotos==1?I18nManager.getText("dialog.correlate.confirmsingle.text"):I18nManager.getText("dialog.correlate.confirmmultiple.text")),
+                               I18nManager.getText("dialog.correlate.title"),
+                               JOptionPane.INFORMATION_MESSAGE);
+                       // observers already informed by track update
+               }
+       }
+
+
        /**
         * Save the coordinates of photos in their exif data
         */
@@ -927,4 +1090,14 @@ public class App
                }
                return num;
        }
+
+       /**
+        * Show a brief help message
+        */
+       public void showHelp()
+       {
+               JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.help.help"),
+                       I18nManager.getText("menu.help"),
+                       JOptionPane.INFORMATION_MESSAGE);
+       }
 }
index 766ed5399fda870f3a740e6e4e2bdd322c9a7aa6..d3fc123174635183d0f9008f7c1d86be30ce8c5f 100644 (file)
@@ -16,6 +16,7 @@ public interface DataSubscriber
 
        /**
         * Inform clients that data has been updated
+        * @param inUpdateType type of update
         */
        public void dataUpdated(byte inUpdateType);
 
index 2511cf7981dcba09eafe474290a314edfff8a6f7..8a95f7f69e3f950eae3ec45cfe539f68cca1ad4b 100644 (file)
@@ -18,12 +18,13 @@ import tim.prune.gui.SelectorDisplay;
 
 /**
  * Tool to visualize, edit and prune GPS data
+ * Please see the included readme.txt or http://activityworkshop.net
  */
 public class GpsPruner
 {
-       // Final release of version 3
-       public static final String VERSION_NUMBER = "3";
-       public static final String BUILD_NUMBER = "074";
+       // Final release of version 4
+       public static final String VERSION_NUMBER = "4";
+       public static final String BUILD_NUMBER = "089";
        private static App APP = null;
 
 
index c056fe24d743ccb5e4cccd4118e21a733fccda02..5af9898ed2c56f5ae1ee949ae19bb71dd25f3d2c 100644 (file)
@@ -19,8 +19,8 @@ public abstract class I18nManager
 
 
        /**
-        * Initialize the library
-        * using the (optional) locale
+        * Initialize the library using the (optional) locale
+        * @param inLocale locale to use, or null for default
         */
        public static void init(Locale inLocale)
        {
diff --git a/tim/prune/correlate/OptionsChangedListener.java b/tim/prune/correlate/OptionsChangedListener.java
new file mode 100644 (file)
index 0000000..70420f7
--- /dev/null
@@ -0,0 +1,98 @@
+package tim.prune.correlate;
+
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+
+/**
+ * Helper class to listen for changed options on the PhotoCorrelator
+ * Tightly coupled but only to ok button and preview function
+ */
+public class OptionsChangedListener implements KeyListener, ActionListener, ItemListener, Runnable
+{
+       /** Correlator object for callbacks */
+       private PhotoCorrelator _correlator;
+       /** Thread counter */
+       private int _threadCount = 0;
+
+       /** Default delay time from change to preview trigger */
+       private static final long PREVIEW_DELAY_TIME = 2500L;
+
+
+       /**
+        * Constructor
+        * @param inCorrelator correlator object for callbacks
+        */
+       public OptionsChangedListener(PhotoCorrelator inCorrelator)
+       {
+               _correlator = inCorrelator;
+       }
+
+       /**
+        * Respond to actions performed on control
+        * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
+        */
+       public void actionPerformed(ActionEvent inEvent)
+       {
+               optionsChanged();
+       }
+
+       /**
+        * Run method, called by separate thread(s)
+        * @see java.lang.Runnable#run()
+        */
+       public void run()
+       {
+               // Wait for a certain time
+               try {
+                       Thread.sleep(PREVIEW_DELAY_TIME);
+               }
+               catch (InterruptedException ie) {}
+               _threadCount--;
+               if (_threadCount == 0) {
+                       // trigger preview (false means automatic)
+                       _correlator.createPreview(false);
+               }
+       }
+
+       /**
+        * Respond to key pressed event
+        * @param inEvent event
+        */
+       public void keyPressed(KeyEvent inEvent)
+       {
+               optionsChanged();
+       }
+
+       /** Ignore key released events */
+       public void keyReleased(KeyEvent inEvent) {}
+
+       /** Ignore key typed events */
+       public void keyTyped(KeyEvent e) {}
+
+       /**
+        * Respond to item change events (eg dropdown)
+        * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
+        */
+       public void itemStateChanged(ItemEvent inEvent)
+       {
+               if (inEvent.getStateChange() == ItemEvent.SELECTED) {
+                       optionsChanged();
+               }
+       }
+
+       /**
+        * Trigger that an option has changed, whatever type
+        */
+       private void optionsChanged()
+       {
+               // disable ok button
+               _correlator.disableOkButton();
+               // start new thread to trigger preview
+               _threadCount++;
+               new Thread(this).start();
+       }
+}
diff --git a/tim/prune/correlate/PhotoCorrelator.java b/tim/prune/correlate/PhotoCorrelator.java
new file mode 100644 (file)
index 0000000..2223772
--- /dev/null
@@ -0,0 +1,661 @@
+package tim.prune.correlate;
+
+import java.awt.BorderLayout;
+import java.awt.CardLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.TreeSet;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+
+import tim.prune.App;
+import tim.prune.I18nManager;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Distance;
+import tim.prune.data.Field;
+import tim.prune.data.Photo;
+import tim.prune.data.PhotoList;
+import tim.prune.data.TimeDifference;
+import tim.prune.data.Timestamp;
+import tim.prune.data.Track;
+import tim.prune.data.TrackInfo;
+
+/**
+ * Class to manage the automatic correlation of photos to points
+ * including the GUI stuff to control the correlation options
+ */
+public class PhotoCorrelator
+{
+       private App _app;
+       private JFrame _parentFrame;
+       private JDialog _dialog;
+       private JButton _nextButton = null, _backButton = null;
+       private JButton _okButton = null;
+       private JPanel _cards = null;
+       private JTable _photoSelectionTable = null;
+       private JLabel _tipLabel = null;
+       private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
+       private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
+       private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
+       private JTextField _limitMinBox = null, _limitSecBox = null;
+       private JTextField _limitDistBox = null;
+       private JComboBox _distUnitsDropdown = null;
+       private JTable _previewTable = null;
+       private boolean _firstTabAvailable = false;
+       private boolean _previewEnabled = false; // flag required to enable preview function on second panel
+
+
+       /**
+        * Constructor
+        * @param inApp App object to report actions to
+        * @param inFrame parent frame for dialogs
+        */
+       public PhotoCorrelator(App inApp, JFrame inFrame)
+       {
+               _app = inApp;
+               _parentFrame = inFrame;
+               _dialog = new JDialog(inFrame, I18nManager.getText("dialog.correlate.title"), true);
+               _dialog.setLocationRelativeTo(inFrame);
+               _dialog.getContentPane().add(makeDialogContents());
+               _dialog.pack();
+       }
+
+
+       /**
+        * Reset dialog and show it
+        */
+       public void begin()
+       {
+               // Check whether track has timestamps, exit if not
+               if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
+               {
+                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
+                               I18nManager.getText("dialog.correlate.title"), JOptionPane.INFORMATION_MESSAGE);
+                       return;
+               }
+               // Check for any non-correlated photos, show warning continue/cancel
+               if (!trackHasUncorrelatedPhotos())
+               {
+                       Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
+                       if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
+                                       I18nManager.getText("dialog.correlate.title"), JOptionPane.YES_NO_OPTION,
+                                       JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
+                               == JOptionPane.NO_OPTION)
+                       {
+                               return;
+                       }
+               }
+               PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
+               _firstTabAvailable = model != null && model.getRowCount() > 0;
+               CardLayout cl = (CardLayout) _cards.getLayout();
+               if (_firstTabAvailable)
+               {
+                       cl.first(_cards);
+                       _nextButton.setEnabled(true);
+                       _backButton.setEnabled(false);
+                       _tipLabel.setVisible(false);
+                       _photoSelectionTable.setModel(model);
+                       _previewEnabled = false;
+                       for (int i=0; i<model.getColumnCount(); i++) {
+                               _photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
+                       }
+                       // Calculate median time difference, select corresponding row of table
+                       int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
+                       _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
+                       _nextButton.requestFocus();
+               }
+               else
+               {
+                       _tipLabel.setVisible(true);
+                       setupSecondCard(null);
+               }
+               _dialog.show();
+       }
+
+
+       /**
+        * Make contents of correlate dialog
+        * @return JPanel containing gui elements
+        */
+       private JPanel makeDialogContents()
+       {
+               JPanel mainPanel = new JPanel();
+               mainPanel.setLayout(new BorderLayout());
+               // Card panel in the middle
+               _cards = new JPanel();
+               _cards.setLayout(new CardLayout());
+
+               // First panel for photo selection table
+               JPanel card1 = new JPanel();
+               card1.setLayout(new BorderLayout(10, 10));
+               card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
+               _photoSelectionTable = new JTable();
+               JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
+               photoScrollPane.setPreferredSize(new Dimension(400, 100));
+               card1.add(photoScrollPane, BorderLayout.CENTER);
+               _cards.add(card1, "card1");
+
+               OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
+               // Second panel for options
+               JPanel card2 = new JPanel();
+               card2.setLayout(new BorderLayout());
+               JPanel card2Top = new JPanel();
+               card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
+               _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
+               card2Top.add(_tipLabel);
+               card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
+               // time offset section
+               JPanel offsetPanel = new JPanel();
+               offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
+               offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
+               JPanel offsetPanelTop = new JPanel();
+               offsetPanelTop.setLayout(new FlowLayout());
+               offsetPanelTop.setBorder(null);
+               offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
+               _offsetHourBox = new JTextField(3);
+               _offsetHourBox.addKeyListener(optionsChangedListener);
+               offsetPanelTop.add(_offsetHourBox);
+               offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
+               _offsetMinBox = new JTextField(3);
+               _offsetMinBox.addKeyListener(optionsChangedListener);
+               offsetPanelTop.add(_offsetMinBox);
+               offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
+               _offsetSecBox = new JTextField(3);
+               _offsetSecBox.addKeyListener(optionsChangedListener);
+               offsetPanelTop.add(_offsetSecBox);
+               offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
+               offsetPanel.add(offsetPanelTop);
+
+               // radio buttons for photo / point later
+               JPanel offsetPanelBot = new JPanel();
+               offsetPanelBot.setLayout(new FlowLayout());
+               offsetPanelBot.setBorder(null);
+               _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
+               _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
+               _photoLaterOption.addItemListener(optionsChangedListener);
+               _pointLaterOption.addItemListener(optionsChangedListener);
+               ButtonGroup laterGroup = new ButtonGroup();
+               laterGroup.add(_photoLaterOption);
+               laterGroup.add(_pointLaterOption);
+               offsetPanelBot.add(_photoLaterOption);
+               offsetPanelBot.add(_pointLaterOption);
+               offsetPanel.add(offsetPanelBot);
+               offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+               card2Top.add(offsetPanel);
+
+               // time limits section
+               JPanel limitsPanel = new JPanel();
+               limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
+               limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
+               JPanel timeLimitPanel = new JPanel();
+               timeLimitPanel.setLayout(new FlowLayout());
+               JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
+               noTimeLimitRadio.addItemListener(optionsChangedListener);
+               timeLimitPanel.add(noTimeLimitRadio);
+               _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
+               _timeLimitRadio.addItemListener(optionsChangedListener);
+               timeLimitPanel.add(_timeLimitRadio);
+               groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
+               _limitMinBox = new JTextField(3);
+               _limitMinBox.addKeyListener(optionsChangedListener);
+               timeLimitPanel.add(_limitMinBox);
+               timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
+               _limitSecBox = new JTextField(3);
+               _limitSecBox.addKeyListener(optionsChangedListener);
+               timeLimitPanel.add(_limitSecBox);
+               timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
+               limitsPanel.add(timeLimitPanel);
+               // distance limits
+               JPanel distLimitPanel = new JPanel();
+               distLimitPanel.setLayout(new FlowLayout());
+               JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
+               noDistLimitRadio.addItemListener(optionsChangedListener);
+               distLimitPanel.add(noDistLimitRadio);
+               _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
+               _distLimitRadio.addItemListener(optionsChangedListener);
+               distLimitPanel.add(_distLimitRadio);
+               groupRadioButtons(noDistLimitRadio, _distLimitRadio);
+               _limitDistBox = new JTextField(4);
+               _limitDistBox.addKeyListener(optionsChangedListener);
+               distLimitPanel.add(_limitDistBox);
+               String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
+                       I18nManager.getText("units.miles")};
+               _distUnitsDropdown = new JComboBox(distUnitsOptions);
+               _distUnitsDropdown.addItemListener(optionsChangedListener);
+               distLimitPanel.add(_distUnitsDropdown);
+               limitsPanel.add(distLimitPanel);
+               limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
+               card2Top.add(limitsPanel);
+
+               // preview button
+               JButton previewButton = new JButton(I18nManager.getText("button.preview"));
+               previewButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               createPreview(true);
+                       }
+               });
+               card2Top.add(previewButton);
+               card2.add(card2Top, BorderLayout.NORTH);
+               // preview
+               _previewTable = new JTable();
+               JScrollPane previewScrollPane = new JScrollPane(_previewTable);
+               previewScrollPane.setPreferredSize(new Dimension(300, 100));
+               card2.add(previewScrollPane, BorderLayout.CENTER);
+               _cards.add(card2, "card2");
+               mainPanel.add(_cards, BorderLayout.CENTER);
+
+               // Button panel at the bottom
+               JPanel buttonPanel = new JPanel();
+               _backButton = new JButton(I18nManager.getText("button.back"));
+               _backButton.addActionListener(new ActionListener()
+                       {
+                               public void actionPerformed(ActionEvent e)
+                               {
+                                       CardLayout cl = (CardLayout) _cards.getLayout();
+                                       cl.previous(_cards);
+                                       _backButton.setEnabled(false);
+                                       _nextButton.setEnabled(true);
+                                       _okButton.setEnabled(false);
+                                       _previewEnabled = false;
+                               }
+                       });
+               _backButton.setEnabled(false);
+               buttonPanel.add(_backButton);
+               _nextButton = new JButton(I18nManager.getText("button.next"));
+               _nextButton.addActionListener(new ActionListener()
+                       {
+                               public void actionPerformed(ActionEvent e)
+                               {
+                                       int rowNum = _photoSelectionTable.getSelectedRow();
+                                       if (rowNum < 0) {rowNum = 0;}
+                                       PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
+                                               .getRow(rowNum);
+                                       setupSecondCard(selectedRow.getTimeDiff());
+                               }
+                       });
+               buttonPanel.add(_nextButton);
+               _okButton = new JButton(I18nManager.getText("button.ok"));
+               _okButton.addActionListener(new ActionListener()
+                       {
+                               public void actionPerformed(ActionEvent e)
+                               {
+                                       _app.finishCorrelatePhotos(getPointPairs());
+                                       _dialog.dispose();
+                               }
+                       });
+               _okButton.setEnabled(false);
+               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);
+               mainPanel.add(buttonPanel, BorderLayout.SOUTH);
+               return mainPanel;
+       }
+
+
+       /**
+        * Construct a table model for the photo selection table
+        * @param inTrackInfo track info object
+        * @return table model
+        */
+       private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
+       {
+               PhotoSelectionTableModel model = new PhotoSelectionTableModel();
+               int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
+               for (int i=0; i<numPhotos; i++)
+               {
+                       Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
+                       if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
+                       {
+                               // Calculate time difference, add to table model
+                               long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
+                               model.addPhoto(photo, timeDiff);
+                       }
+               }
+               return model;
+       }
+
+
+       /**
+        * Group the two radio buttons together with a ButtonGroup
+        * @param inButton1 first radio button
+        * @param inButton2 second radio button
+        */
+       private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
+       {
+               ButtonGroup buttonGroup = new ButtonGroup();
+               buttonGroup.add(inButton1);
+               buttonGroup.add(inButton2);
+               inButton1.setSelected(true);
+       }
+
+
+       /**
+        * Set up the second card using the given time difference and show it
+        * @param inTimeDiff time difference to use for photo time offsets
+        */
+       private void setupSecondCard(TimeDifference inTimeDiff)
+       {
+               _previewEnabled = false;
+               boolean hasTimeDiff = inTimeDiff != null;
+               if (!hasTimeDiff)
+               {
+                       // No time difference available, so calculate based on computer's time zone
+                       inTimeDiff = getTimezoneOffset();
+               }
+               // Use time difference to set edit boxes
+               _offsetHourBox.setText("" + inTimeDiff.getNumHours());
+               _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
+               _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
+               _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
+               _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
+               createPreview(inTimeDiff, true);
+               CardLayout cl = (CardLayout) _cards.getLayout();
+               cl.next(_cards);
+               _backButton.setEnabled(hasTimeDiff);
+               _nextButton.setEnabled(false);
+               // enable ok button if any photos have been selected
+               _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
+               _previewEnabled = true;
+       }
+
+
+       /**
+        * Create a preview of the correlate action using the selected time difference
+        * @param inFromButton true if triggered from button press, false if automatic
+        */
+       public void createPreview(boolean inFromButton)
+       {
+               // Exit if still on first panel
+               if (!_previewEnabled) {return;}
+               // Create a TimeDifference based on the edit boxes
+               int numHours = getValue(_offsetHourBox.getText());
+               int numMins = getValue(_offsetMinBox.getText());
+               int numSecs = getValue(_offsetSecBox.getText());
+               boolean isPos = _photoLaterOption.isSelected();
+               createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
+       }
+
+
+       /**
+        * Create a preview of the correlate action using the selected time difference
+        * @param inTimeDiff TimeDifference to use for preview
+        * @param inShowWarning true to show warning if all points out of range
+        */
+       private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
+       {
+               TimeDifference timeLimit = parseTimeLimit();
+               double angDistLimit = parseDistanceLimit();
+               PhotoPreviewTableModel model = new PhotoPreviewTableModel();
+               PhotoList photos = _app.getTrackInfo().getPhotoList();
+               // Loop through photos deciding whether to set correlate flag or not
+               int numPhotos = photos.getNumPhotos();
+               for (int i=0; i<numPhotos; i++)
+               {
+                       Photo photo = photos.getPhoto(i);
+                       PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
+                       PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
+                       // Don't try to correlate photos which don't have points either side
+                       boolean correlatePhoto = pair.isValid();
+                       // Check time limits, distance limits
+                       if (timeLimit != null && correlatePhoto) {
+                               long numSecs = pair.getMinSeconds();
+                               correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
+                       }
+                       if (angDistLimit > 0.0 && correlatePhoto) {
+                               final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
+                               //System.out.println("(dist between pair is " + angDistPair + ") which means "
+                               //      + Distance.convertRadiansToDistance(angDistPair, Distance.UNITS_METRES) + "m");
+                               double frac = pair.getFraction();
+                               if (frac > 0.5) {frac = 1 - frac;}
+                               final double angDistPhoto = angDistPair * frac;
+                               correlatePhoto = (angDistPhoto < angDistLimit);
+                       }
+                       // Don't select photos which are already correlated to the same point
+                       if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
+                               && pair.getPointBefore().getPhoto().equals(photo)) {
+                               correlatePhoto = false;
+                       }
+                       row.setCorrelateFlag(correlatePhoto);
+                       model.addPhotoRow(row);
+               }
+               _previewTable.setModel(model);
+               // Set distance units
+               model.setDistanceUnits(getSelectedDistanceUnits());
+               // Set column widths
+               _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
+               final int[] colWidths = {150, 160, 100, 100, 50};
+               for (int i=0; i<model.getColumnCount(); i++) {
+                       _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
+               }
+               // check if any photos found
+               _okButton.setEnabled(model.hasPhotosSelected());
+               if (inShowWarning && !model.hasPhotosSelected())
+               {
+                       JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
+                               I18nManager.getText("dialog.correlate.title"), JOptionPane.ERROR_MESSAGE);
+               }
+       }
+
+       /**
+        * Parse the time limit values entered and validate them
+        * @return TimeDifference object describing limit
+        */
+       private TimeDifference parseTimeLimit()
+       {
+               if (!_timeLimitRadio.isSelected()) {return null;}
+               int mins = getValue(_limitMinBox.getText());
+               _limitMinBox.setText("" + mins);
+               int secs = getValue(_limitSecBox.getText());
+               _limitSecBox.setText("" + secs);
+               if (mins <= 0 && secs <= 0) {return null;}
+               return new TimeDifference(0, mins, secs, true);
+       }
+
+       /**
+        * Parse the distance limit value entered and validate
+        * @return angular distance in radians
+        */
+       private double parseDistanceLimit()
+       {
+               double value = -1.0;
+               if (_distLimitRadio.isSelected())
+               {
+                       try
+                       {
+                               value = Double.parseDouble(_limitDistBox.getText());
+                       }
+                       catch (NumberFormatException nfe) {}
+               }
+               if (value <= 0.0) {
+                       _limitDistBox.setText("0");
+                       return -1.0;
+               }
+               _limitDistBox.setText("" + value);
+               return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
+       }
+
+
+       /**
+        * @return the selected distance units from the dropdown
+        */
+       private int getSelectedDistanceUnits()
+       {
+               final int[] distUnits = {Distance.UNITS_KILOMETRES, Distance.UNITS_METRES, Distance.UNITS_MILES};
+               return distUnits[_distUnitsDropdown.getSelectedIndex()];
+       }
+
+
+       /**
+        * Try to parse the given string
+        * @param inText String to parse
+        * @return value if parseable, 0 otherwise
+        */
+       private static int getValue(String inText)
+       {
+               int value = 0;
+               try {
+                       value = Integer.parseInt(inText);
+               }
+               catch (NumberFormatException nfe) {}
+               return value;
+       }
+
+
+       /**
+        * Get the point pair surrounding the given photo
+        * @param inTrack track object
+        * @param inPhoto photo object
+        * @param inOffset time offset to apply to photos
+        * @return point pair resulting from correlation
+        */
+       private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
+       {
+               PointPair pair = new PointPair(inPhoto);
+               // Add offet to photo timestamp
+               Timestamp photoStamp = inPhoto.getTimestamp().subtractOffset(inOffset);
+               int numPoints = inTrack.getNumPoints();
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = inTrack.getPoint(i);
+                       Timestamp pointStamp = point.getTimestamp();
+                       if (pointStamp != null && pointStamp.isValid())
+                       {
+                               long numSeconds = pointStamp.getSecondsSince(photoStamp);
+                               pair.addPoint(point, numSeconds);
+                       }
+               }
+               return pair;
+       }
+
+
+       /**
+        * Construct an array of the point pairs to use for correlation
+        * @return array of PointPair objects
+        */
+       private PointPair[] getPointPairs()
+       {
+               PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
+               int numPhotos = model.getRowCount();
+               PointPair[] pairs = new PointPair[numPhotos];
+               // Loop over photos in preview table model
+               for (int i=0; i<numPhotos; i++)
+               {
+                       PhotoPreviewTableRow row = model.getRow(i);
+                       // add all selected pairs to array (other elements remain null)
+                       if (row.getCorrelateFlag().booleanValue())
+                       {
+                               pairs[i] = row.getPointPair();
+                       }
+               }
+               return pairs;
+       }
+
+       /**
+        * @return time difference of local time zone from UTC when the first photo was taken
+        */
+       private TimeDifference getTimezoneOffset()
+       {
+               Calendar cal = null;
+               // Base time difference on DST when first photo was taken
+               Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
+               if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
+                       cal = firstPhoto.getTimestamp().getCalendar();
+               }
+               else {
+                       // No photo or no timestamp, just use current time
+                       cal = Calendar.getInstance();
+               }
+               // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
+               TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
+               return timeDiff;
+       }
+
+
+       /**
+        * Calculate the median index to select from the table
+        * @param inModel table model
+        * @return index of entry to select from table
+        */
+       private static int getMedianIndex(PhotoSelectionTableModel inModel)
+       {
+               // make sortable list
+               TreeSet set = new TreeSet();
+               // loop through rows of table adding to list
+               int numRows = inModel.getRowCount();
+               int i;
+               for (i=0; i<numRows; i++)
+               {
+                       PhotoSelectionTableRow row = inModel.getRow(i);
+                       set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
+                       //System.out.println("pair " + i + " has time " + row.getTimeDiff().getTotalSeconds());
+               }
+               // pull out middle entry and return index
+               TimeIndexPair pair = null;
+               Iterator iterator = set.iterator();
+               for (i=0; i<(numRows+1)/2; i++)
+               {
+                       pair = (TimeIndexPair) iterator.next();
+                       //System.out.println("After sorting, pair " + i + " has index " + pair.getIndex());
+               }
+               return pair.getIndex();
+       }
+
+
+       /**
+        * Disable the ok button
+        */
+       public void disableOkButton()
+       {
+               if (_okButton != null)
+               {
+                       _okButton.setEnabled(false);
+               }
+       }
+
+
+       /**
+        * Check if the track has any uncorrelated photos
+        * @return true if there are any photos which are not connected to points
+        */
+       private boolean trackHasUncorrelatedPhotos()
+       {
+               PhotoList photoList = _app.getTrackInfo().getPhotoList();
+               int numPhotos = photoList.getNumPhotos();
+               // loop over photos
+               for (int i=0; i<numPhotos; i++)
+               {
+                       Photo photo = photoList.getPhoto(i);
+                       if (photo != null && photo.getDataPoint() == null) {
+                               return true;
+                       }
+               }
+               // no uncorrelated photos found
+               return false;
+       }
+}
diff --git a/tim/prune/correlate/PhotoPreviewTableModel.java b/tim/prune/correlate/PhotoPreviewTableModel.java
new file mode 100644 (file)
index 0000000..aed044d
--- /dev/null
@@ -0,0 +1,188 @@
+package tim.prune.correlate;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import javax.swing.table.AbstractTableModel;
+import tim.prune.I18nManager;
+import tim.prune.data.Distance;
+
+/**
+ * Class to act as table model for the photo preview table
+ */
+public class PhotoPreviewTableModel extends AbstractTableModel
+{
+       /** ArrayList containing TableRow objects */
+       private ArrayList _list = new ArrayList();
+       /** Distance units */
+       private int _distanceUnits = Distance.UNITS_KILOMETRES;
+       /** Number formatter */
+       private static final NumberFormat FORMAT_ONE_DP = NumberFormat.getNumberInstance();
+
+
+       /** Static block to initialise the one d.p. formatter */
+       static
+       {
+               FORMAT_ONE_DP.setMaximumFractionDigits(1);
+               FORMAT_ONE_DP.setMinimumFractionDigits(1);
+       }
+
+
+       /**
+        * @return the column count, always 5
+        */
+       public int getColumnCount()
+       {
+               return 5;
+       }
+
+
+       /**
+        * Get the name of the column
+        * @param inColNum column number
+        * @return column name
+        */
+       public String getColumnName(int inColNum)
+       {
+               if (inColNum == 0) return I18nManager.getText("dialog.correlate.photoselect.photoname");
+               else if (inColNum == 1) return I18nManager.getText("fieldname.timestamp");
+               else if (inColNum == 2) return I18nManager.getText("dialog.correlate.photoselect.timediff");
+               else if (inColNum == 3) return I18nManager.getText("fieldname.distance");
+               return I18nManager.getText("dialog.correlate.options.correlate");
+       }
+
+
+       /**
+        * @return the row count
+        */
+       public int getRowCount()
+       {
+               return _list.size();
+       }
+
+
+       /**
+        * Get the selected row from the table
+        * @param inRowIndex row index
+        * @return table row object
+        */
+       public PhotoPreviewTableRow getRow(int inRowIndex)
+       {
+               PhotoPreviewTableRow row = (PhotoPreviewTableRow) _list.get(inRowIndex);
+               return row;
+       }
+
+
+       /**
+        * Get the value of the specified cell
+        * @param inRowIndex row index
+        * @param inColumnIndex column index
+        * @return value of specified cell
+        */
+       public Object getValueAt(int inRowIndex, int inColumnIndex)
+       {
+               PhotoPreviewTableRow row = (PhotoPreviewTableRow) _list.get(inRowIndex);
+               if (inColumnIndex == 0) return row.getPhoto().getFile().getName();
+               else if (inColumnIndex == 1) {
+                       return row.getPhoto().getTimestamp().getText();
+               }
+               else if (inColumnIndex == 2) {
+                       if (row.getPointPair().isValid()) {
+                               return row.getTimeDiff().getDescription();
+                       }
+                       return "";
+               }
+               else if (inColumnIndex == 3) {
+                       if (row.getPointPair().isValid()) {
+                               return FORMAT_ONE_DP.format(row.getDistance(_distanceUnits));
+                       }
+                       return "";
+               }
+               return row.getCorrelateFlag();
+       }
+
+
+       /**
+        * @param inUnits the distance units to use
+        */
+       public void setDistanceUnits(int inUnits)
+       {
+               _distanceUnits = inUnits;
+       }
+
+
+       /**
+        * Clear the list
+        */
+       public void reset()
+       {
+               _list.clear();
+       }
+
+
+       /**
+        * Add a photo to the list
+        * @param inRow row to add
+        */
+       public void addPhotoRow(PhotoPreviewTableRow inRow)
+       {
+               _list.add(inRow);
+       }
+
+
+       /**
+        * Get the class of objects in the given column
+        * @see javax.swing.table.AbstractTableModel#getColumnClass(int)
+        */
+       public Class getColumnClass(int inColumnIndex)
+       {
+               if (inColumnIndex == 4) {return Boolean.class;}
+               return super.getColumnClass(inColumnIndex);
+       }
+
+
+       /**
+        * Get whether the given cell is editable
+        * @see javax.swing.table.AbstractTableModel#isCellEditable(int, int)
+        */
+       public boolean isCellEditable(int inRowIndex, int inColumnIndex)
+       {
+               if (inColumnIndex == 4) {return true;}
+               return super.isCellEditable(inRowIndex, inColumnIndex);
+       }
+
+
+       /**
+        * @return true if any of the correlate flags are on
+        */
+       public boolean hasPhotosSelected()
+       {
+               for (int i=0; i<getRowCount(); i++)
+               {
+                       if (getRow(i).getCorrelateFlag().booleanValue())
+                       {
+                               return true;
+                       }
+               }
+               // None switched on
+               return false;
+       }
+
+
+       /**
+        * Set the value at the given table cell
+        * @see javax.swing.table.AbstractTableModel#setValueAt(java.lang.Object, int, int)
+        */
+       public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex)
+       {
+               // can only edit the correlate column
+               if (inColumnIndex == 4)
+               {
+                       PhotoPreviewTableRow row = getRow(inRowIndex);
+                       // Don't allow setting of photos which can't be correlated
+                       if (row.getPointPair().isValid())
+                       {
+                               row.setCorrelateFlag(((Boolean) inValue).booleanValue());
+                       }
+               }
+       }
+}
diff --git a/tim/prune/correlate/PhotoPreviewTableRow.java b/tim/prune/correlate/PhotoPreviewTableRow.java
new file mode 100644 (file)
index 0000000..9df0b52
--- /dev/null
@@ -0,0 +1,70 @@
+package tim.prune.correlate;
+
+import tim.prune.data.Distance;
+
+/**
+ * Class to hold contents of a single row
+ * in the photo preview table
+ */
+public class PhotoPreviewTableRow extends PhotoSelectionTableRow
+{
+       private PointPair _pointPair = null;
+       private double _distance = 0.0;
+       private int _status = 0;
+       private boolean _correlate = false;
+
+
+       /**
+        * Constructor
+        * @param inPointPair point pair object
+        */
+       public PhotoPreviewTableRow(PointPair inPointPair)
+       {
+               super(inPointPair.getPhoto(), inPointPair.getMinSeconds());
+               _pointPair = inPointPair;
+               _distance = inPointPair.getMinRadians();
+               _status = 0;
+               _correlate = (inPointPair.getPhoto().getDataPoint() == null);
+       }
+
+       /**
+        * @param inUnits units to use
+        * @return distance in selected format
+        */
+       public double getDistance(int inUnits)
+       {
+               return Distance.convertRadiansToDistance(_distance, inUnits);
+       }
+
+       /**
+        * @return point status
+        */
+       public int getStatus()
+       {
+               return _status;
+       }
+
+       /**
+        * @return point pair object
+        */
+       public PointPair getPointPair()
+       {
+               return _pointPair;
+       }
+
+       /**
+        * @return flag to set whether to correlate or not
+        */
+       public Boolean getCorrelateFlag()
+       {
+               return Boolean.valueOf(_correlate);
+       }
+
+       /**
+        * @param inFlag true to correlate, false to ignore
+        */
+       public void setCorrelateFlag(boolean inFlag)
+       {
+               _correlate = inFlag;
+       }
+}
diff --git a/tim/prune/correlate/PhotoSelectionTableModel.java b/tim/prune/correlate/PhotoSelectionTableModel.java
new file mode 100644 (file)
index 0000000..2e9d315
--- /dev/null
@@ -0,0 +1,96 @@
+package tim.prune.correlate;
+
+import java.util.ArrayList;
+import javax.swing.table.AbstractTableModel;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Photo;
+
+/**
+ * Class to act as table model for the photo selection table
+ */
+public class PhotoSelectionTableModel extends AbstractTableModel
+{
+       private ArrayList _list = new ArrayList();
+
+
+       /**
+        * @return the column count, always 4
+        */
+       public int getColumnCount()
+       {
+               return 4;
+       }
+
+
+       /**
+        * Get the name of the column
+        * @param inColNum column number
+        * @return column name
+        */
+       public String getColumnName(int inColNum)
+       {
+               if (inColNum == 0) return I18nManager.getText("dialog.correlate.photoselect.photoname");
+               else if (inColNum == 1) return I18nManager.getText("fieldname.timestamp");
+               else if (inColNum == 2) return I18nManager.getText("dialog.correlate.photoselect.timediff");
+               return I18nManager.getText("dialog.correlate.photoselect.photolater");
+       }
+
+
+       /**
+        * @return the row count
+        */
+       public int getRowCount()
+       {
+               return _list.size();
+       }
+
+
+       /**
+        * Get the selected row from the table
+        * @param inRowIndex row index
+        * @return table row object
+        */
+       public PhotoSelectionTableRow getRow(int inRowIndex)
+       {
+               PhotoSelectionTableRow row = (PhotoSelectionTableRow) _list.get(inRowIndex);
+               return row;
+       }
+
+
+       /**
+        * Get the value of the specified cell
+        * @param inRowIndex row index
+        * @param inColumnIndex column index
+        * @return value of specified cell
+        */
+       public Object getValueAt(int inRowIndex, int inColumnIndex)
+       {
+               // TODO: only show time of photos (not date) if dates all identical
+               PhotoSelectionTableRow row = (PhotoSelectionTableRow) _list.get(inRowIndex);
+               if (inColumnIndex == 0) return row.getPhoto().getFile().getName();
+               else if (inColumnIndex == 1) return row.getPhoto().getTimestamp().getText();
+               else if (inColumnIndex == 2) return row.getTimeDiff().getDescription();
+               return (row.getTimeDiff().getIsPositive() ? I18nManager.getText("dialog.about.yes") :
+                       I18nManager.getText("dialog.about.no"));
+       }
+
+
+       /**
+        * Clear the list
+        */
+       public void reset()
+       {
+               _list.clear();
+       }
+
+       /**
+        * Add a photo to the list
+        * @param inPhoto photo to add
+        * @param inTimeDiff time difference
+        */
+       public void addPhoto(Photo inPhoto, long inTimeDiff)
+       {
+               _list.add(new PhotoSelectionTableRow(inPhoto, inTimeDiff));
+       }
+}
diff --git a/tim/prune/correlate/PhotoSelectionTableRow.java b/tim/prune/correlate/PhotoSelectionTableRow.java
new file mode 100644 (file)
index 0000000..e818bbe
--- /dev/null
@@ -0,0 +1,41 @@
+package tim.prune.correlate;
+
+import tim.prune.data.Photo;
+import tim.prune.data.TimeDifference;
+
+/**
+ * Class to hold contents of a single row
+ * in the photo selection table
+ */
+public class PhotoSelectionTableRow
+{
+       private Photo _photo = null;
+       private TimeDifference _timeDiff = null;
+
+       /**
+        * Constructor
+        * @param inPhoto Photo object
+        * @param inNumSecs number of seconds time difference as long
+        */
+       public PhotoSelectionTableRow(Photo inPhoto, long inNumSecs)
+       {
+               _photo = inPhoto;
+               _timeDiff = new TimeDifference(inNumSecs);
+       }
+
+       /**
+        * @return Photo object
+        */
+       public Photo getPhoto()
+       {
+               return _photo;
+       }
+
+       /**
+        * @return time difference
+        */
+       public TimeDifference getTimeDiff()
+       {
+               return _timeDiff;
+       }
+}
diff --git a/tim/prune/correlate/PointPair.java b/tim/prune/correlate/PointPair.java
new file mode 100644 (file)
index 0000000..9b27ff9
--- /dev/null
@@ -0,0 +1,135 @@
+package tim.prune.correlate;
+
+import tim.prune.data.DataPoint;
+import tim.prune.data.Photo;
+
+/**
+ * Class to hold a pair of points
+ * used to hold the result of correlation of a photo
+ */
+public class PointPair
+{
+       private Photo _photo = null;
+       private DataPoint _pointBefore = null;
+       private DataPoint _pointAfter = null;
+       private long _secondsBefore = 1L;
+       private long _secondsAfter = -1L;
+
+
+       /**
+        * Constructor
+        * @param inPhoto Photo object
+        */
+       public PointPair(Photo inPhoto)
+       {
+               _photo = inPhoto;
+       }
+
+
+       /**
+        * Add a point to the pair
+        * @param inPoint data point
+        * @param inSeconds number of seconds time difference, positive means point later
+        */
+       public void addPoint(DataPoint inPoint, long inSeconds)
+       {
+               // Check if point is closest point before
+               if (inSeconds <= 0)
+               {
+                       // point stamp is before photo stamp
+                       if (inSeconds > _secondsBefore || _secondsBefore > 0L)
+                       {
+                               // point stamp is nearer to photo
+                               _pointBefore = inPoint;
+                               _secondsBefore = inSeconds;
+                       }
+               }
+               // Check if point is closest point after
+               if (inSeconds >= 0)
+               {
+                       // point stamp is after photo stamp
+                       if (inSeconds < _secondsAfter || _secondsAfter < 0L)
+                       {
+                               // point stamp is nearer to photo
+                               _pointAfter = inPoint;
+                               _secondsAfter = inSeconds;
+                       }
+               }
+       }
+
+
+       /**
+        * @return Photo object
+        */
+       public Photo getPhoto()
+       {
+               return _photo;
+       }
+
+       /**
+        * @return the closest point before the photo
+        */
+       public DataPoint getPointBefore()
+       {
+               return _pointBefore;
+       }
+
+       /**
+        * @return number of seconds between photo and subsequent point
+        */
+       public long getSecondsBefore()
+       {
+               return _secondsBefore;
+       }
+
+       /**
+        * @return the closest point after the photo
+        */
+       public DataPoint getPointAfter()
+       {
+               return _pointAfter;
+       }
+
+       /**
+        * @return number of seconds between previous point and photo
+        */
+       public long getSecondsAfter()
+       {
+               return _secondsAfter;
+       }
+
+       /**
+        * @return true if both points found
+        */
+       public boolean isValid()
+       {
+               return getPointBefore() != null && getPointAfter() != null;
+       }
+
+       /**
+        * @return the fraction of the distance along the interpolated line
+        */
+       public double getFraction()
+       {
+               if (_secondsAfter == 0L) return 0.0;
+               return (-_secondsBefore * 1.0 / (-_secondsBefore + _secondsAfter));
+       }
+
+       /**
+        * @return the number of seconds to the nearest point
+        */
+       public long getMinSeconds()
+       {
+               return Math.min(_secondsAfter, -_secondsBefore);
+       }
+
+       /**
+        * @return angle from photo to nearest point in radians
+        */
+       public double getMinRadians()
+       {
+               double totalRadians = DataPoint.calculateRadiansBetween(_pointBefore, _pointAfter);
+               double frac = getFraction();
+               return totalRadians * Math.min(frac, 1-frac);
+       }
+}
diff --git a/tim/prune/correlate/TimeIndexPair.java b/tim/prune/correlate/TimeIndexPair.java
new file mode 100644 (file)
index 0000000..50305d4
--- /dev/null
@@ -0,0 +1,45 @@
+package tim.prune.correlate;
+
+/**
+ * Simple class to hold a time and an index.
+ * Used in a TreeSet for calculating median time difference
+ */
+public class TimeIndexPair implements Comparable
+{
+       /** Time as long */
+       private long _time = 0L;
+       /** Index as int */
+       private int _index = 0;
+
+
+       /**
+        * Constructor
+        * @param inTime time as long
+        * @param inIndex index as int
+        */
+       public TimeIndexPair(long inTime, int inIndex)
+       {
+               _time = inTime;
+               _index = inIndex;
+       }
+
+
+       /**
+        * @return the index
+        */
+       public int getIndex()
+       {
+               return _index;
+       }
+
+
+       /**
+        * Compare two TimeIndexPair objects
+        * @see java.lang.Comparable#compareTo(java.lang.Object)
+        */
+       public int compareTo(Object inOther)
+       {
+               TimeIndexPair other = (TimeIndexPair) inOther;
+               return (int) (_time - other._time);
+       }
+}
index 54a3f68eb862416791840dcfcb84009a5d191499..af247cfe0217d07acce4791832a39e0207298b6e 100644 (file)
@@ -17,7 +17,9 @@ public class Altitude
 
 
        /**
-        * Constructor
+        * Constructor using String
+        * @param inString string to parse
+        * @param inFormat format of altitude, either metres or feet
         */
        public Altitude(String inString, int inFormat)
        {
@@ -35,7 +37,9 @@ public class Altitude
 
 
        /**
-        * Constructor
+        * Constructor with int vaue
+        * @param inValue int value of altitude
+        * @param inFormat format of altitude, either metres or feet
         */
        public Altitude(int inValue, int inFormat)
        {
@@ -99,6 +103,19 @@ public class Altitude
         * @return Interpolated Altitude object
         */
        public static Altitude interpolate(Altitude inStart, Altitude inEnd, int inIndex, int inNumSteps)
+       {
+               return interpolate(inStart, inEnd, 1.0 * (inIndex + 1) / (inNumSteps + 1));
+       }
+
+
+       /**
+        * Interpolate a new Altitude object between the given ones
+        * @param inStart start altitude
+        * @param inEnd end altitude
+        * @param inFrac fraction of distance from first point
+        * @return Interpolated Altitude object
+        */
+       public static Altitude interpolate(Altitude inStart, Altitude inEnd, double inFrac)
        {
                // Check if altitudes are valid
                if (inStart == null || inEnd == null || !inStart.isValid() || !inEnd.isValid())
@@ -107,8 +124,8 @@ public class Altitude
                int altFormat = inStart.getFormat();
                int startValue = inStart.getValue();
                int endValue = inEnd.getValue(altFormat);
-               int newValue = startValue
-                       + (int) ((endValue - startValue) * 1.0 / (inNumSteps + 1) * (inIndex + 1));
+               // interpolate between start and end
+               int newValue = startValue + (int) ((endValue - startValue) * inFrac);
                return new Altitude(newValue, altFormat);
        }
 }
index 6c7e5d55a286c1c0715227cc4a1a7204a9a164d2..aff974d1109231707f56397750f2388d5e005b13 100644 (file)
@@ -22,7 +22,7 @@ public class AltitudeRange
 
        /**
         * Add a value to the range
-        * @param inValue value to add, only positive values considered
+        * @param inAltitude value to add, only positive values considered
         */
        public void addValue(Altitude inAltitude)
        {
index 8a81850cba90e7c429696a3062dc3e5f96681a66..7a572688ed2a0eb0eb63e6a3e6fda5f9037554d9 100644 (file)
@@ -6,6 +6,7 @@ package tim.prune.data;
  */
 public abstract class Coordinate
 {
+       public static final int NO_CARDINAL = -1;
        public static final int NORTH = 0;
        public static final int EAST = 1;
        public static final int SOUTH = 2;
@@ -27,6 +28,7 @@ public abstract class Coordinate
        private int _minutes = 0;
        private int _seconds = 0;
        private int _fracs = 0;
+       private int _fracDenom = 0;
        private String _originalString = null;
        private int _originalFormat = FORMAT_NONE;
        private double _asDouble = 0.0;
@@ -47,16 +49,18 @@ public abstract class Coordinate
                }
                if (strLen > 1)
                {
-                       // Check for leading character NSEW
-                       _cardinal = getCardinal(inString.charAt(0));
+                       // Check for cardinal character either at beginning or end
+                       _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
                        // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
                        int numFields = 0;
                        boolean inNumeric = false;
                        char currChar;
-                       long[] fields = new long[4];
+                       long[] fields = new long[4]; // needs to be long for lengthy decimals
                        long[] denoms = new long[4];
+                       String secondDelim = "";
                        try
                        {
+                               // Loop over characters in input string, populating fields array
                                for (int i=0; i<strLen; i++)
                                {
                                        currChar = inString.charAt(i);
@@ -74,6 +78,10 @@ public abstract class Coordinate
                                        else
                                        {
                                                inNumeric = false;
+                                               // Remember second delimiter
+                                               if (numFields == 2) {
+                                                       secondDelim += currChar;
+                                               }
                                        }
                                }
                                _valid = (numFields > 0);
@@ -86,6 +94,7 @@ public abstract class Coordinate
                        // parse fields according to number found
                        _degrees = (int) fields[0];
                        _originalFormat = FORMAT_DEG;
+                       _fracDenom = 10;
                        if (numFields == 2)
                        {
                                // String is just decimal degrees
@@ -95,7 +104,8 @@ public abstract class Coordinate
                                _seconds = (int) numSecs;
                                _fracs = (int) ((numSecs - _seconds) * 10);
                        }
-                       else if (numFields == 3)
+                       // Differentiate between d-m.f and d-m-s using . or ,
+                       else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
                        {
                                // String is degrees-minutes.fractions
                                _originalFormat = FORMAT_DEG_MIN;
@@ -104,28 +114,63 @@ public abstract class Coordinate
                                _seconds = (int) numSecs;
                                _fracs = (int) ((numSecs - _seconds) * 10);
                        }
-                       else if (numFields == 4)
+                       else if (numFields == 4 || numFields == 3)
                        {
-                               _originalFormat = FORMAT_DEG_MIN_SEC;
                                // String is degrees-minutes-seconds.fractions
+                               _originalFormat = FORMAT_DEG_MIN_SEC;
                                _minutes = (int) fields[1];
                                _seconds = (int) fields[2];
                                _fracs = (int) fields[3];
+                               _fracDenom = (int) denoms[3];
+                               if (_fracDenom < 1) {_fracDenom = 1;}
                        }
-                       _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 36000.0);
+                       _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
                        if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
                                _asDouble = -_asDouble;
+                       // validate fields
+                       _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
                }
                else _valid = false;
        }
 
 
+       /**
+        * Get the cardinal from the given character
+        * @param inFirstChar first character from file
+        * @param inLastChar last character from file
+        */
+       protected int getCardinal(char inFirstChar, char inLastChar)
+       {
+               // Try leading character first
+               int cardinal = getCardinal(inFirstChar);
+               // if not there, try trailing character
+               if (cardinal == NO_CARDINAL) {
+                       cardinal = getCardinal(inLastChar);
+               }
+               // use default from concrete subclass
+               if (cardinal == NO_CARDINAL) {
+                       cardinal = getDefaultCardinal();
+               }
+               return cardinal;
+       }
+
+
        /**
         * Get the cardinal from the given character
         * @param inChar character from file
         */
        protected abstract int getCardinal(char inChar);
 
+       /**
+        * @return the default cardinal for the subclass
+        */
+       protected abstract int getDefaultCardinal();
+
+       /**
+        * @return the maximum degree range for this coordinate
+        */
+       protected abstract int getMaxDegrees();
+
 
        /**
         * Constructor
@@ -143,6 +188,7 @@ public abstract class Coordinate
                double numSecs = (numMins - _minutes) * 60.0;
                _seconds = (int) numSecs;
                _fracs = (int) ((numSecs - _seconds) * 10);
+               _fracDenom = 10; // fixed for now
                // Make a string to display on screen
                _cardinal = inCardinal;
                _originalFormat = FORMAT_NONE;
@@ -186,7 +232,6 @@ public abstract class Coordinate
 
        /**
         * Output the Coordinate in the given format
-        * @param inOriginalString the original String to use as default
         * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
         * @return String for output
         */
@@ -206,32 +251,33 @@ public abstract class Coordinate
                                                .append(threeDigitString(_degrees)).append('°')
                                                .append(twoDigitString(_minutes)).append('\'')
                                                .append(twoDigitString(_seconds)).append('.')
-                                               .append(_fracs);
+                                               .append(formatFraction(_fracs, _fracDenom));
                                        answer = buffer.toString();
                                        break;
                                }
                                case FORMAT_DEG_MIN:
                                {
                                        answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
-                                               + (_minutes + _seconds / 60.0 + _fracs / 600.0) + "'";
+                                               + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
                                        break;
                                }
                                case FORMAT_DEG_WHOLE_MIN:
                                {
                                        answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
-                                               + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 600.0 + 0.5) + "'";
+                                               + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 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);
+                                               + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
                                        break;
                                }
                                case FORMAT_DEG_MIN_SEC_WITH_SPACES:
                                {
-                                       answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + _fracs;
+                                       // Note: cardinal not needed as this format is only for exif, which has cardinal separately
+                                       answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
                                        break;
                                }
                                case FORMAT_CARDINAL:
@@ -244,6 +290,22 @@ public abstract class Coordinate
                return answer;
        }
 
+       /**
+        * Format the fraction part of seconds value
+        * @param inFrac fractional part eg 123
+        * @param inDenom denominator of fraction eg 10000
+        * @return String describing fraction, in this case 0123
+        */
+       private static final String formatFraction(int inFrac, int inDenom)
+       {
+               if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
+               String denomString = "" + inDenom;
+               int reqdLen = denomString.length() - 1;
+               String result = denomString + inFrac;
+               int resultLen = result.length();
+               return result.substring(resultLen - reqdLen);
+       }
+
 
        /**
         * Format an integer to a two-digit String
@@ -283,10 +345,24 @@ public abstract class Coordinate
         */
        public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
                int inIndex, int inNumPoints)
+       {
+               return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
+       }
+
+
+       /**
+        * Create a new Coordinate between two others
+        * @param inStart start coordinate
+        * @param inEnd end coordinate
+        * @param inFraction fraction from start to end
+        * @return new Coordinate object
+        */
+       public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
+               double inFraction)
        {
                double startValue = inStart.getDouble();
                double endValue = inEnd.getDouble();
-               double newValue = startValue + (endValue - startValue) * (inIndex+1) / (inNumPoints + 1);
+               double newValue = startValue + (endValue - startValue) * inFraction;
                Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
                return answer;
        }
@@ -303,9 +379,11 @@ public abstract class Coordinate
 
        /**
         * Create a String representation for debug
+        * @return String describing coordinate value
         */
        public String toString()
        {
-               return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "." + _fracs + ") = " + _asDouble;
+               return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
+                       + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
        }
 }
index 46c4acd9d1ad562883ed9d8a8830802d1f123c19..5e864ce10e821068310932621f24c011be9e3097 100644 (file)
@@ -138,30 +138,37 @@ public class DataPoint
        }
 
 
+       /** @return latitude */
        public Coordinate getLatitude()
        {
                return _latitude;
        }
+       /** @return longitude */
        public Coordinate getLongitude()
        {
                return _longitude;
        }
+       /** @return true if point has altitude */
        public boolean hasAltitude()
        {
                return _altitude.isValid();
        }
+       /** @return altitude */
        public Altitude getAltitude()
        {
                return _altitude;
        }
+       /** @return true if point has timestamp */
        public boolean hasTimestamp()
        {
                return _timestamp.isValid();
        }
+       /** @return timestamp */
        public Timestamp getTimestamp()
        {
                return _timestamp;
        }
+       /** @return waypoint name, if any */
        public String getWaypointName()
        {
                return _waypointName;
@@ -202,10 +209,7 @@ public class DataPoint
                {
                        return !inOther.isWaypoint();
                }
-               else
-               {
-                       return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
-               }
+               return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
        }
 
 
@@ -246,21 +250,33 @@ public class DataPoint
        public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
        {
                DataPoint[] range = new DataPoint[inNumPoints];
-               Coordinate endLatitude = inEndPoint.getLatitude();
-               Coordinate endLongitude = inEndPoint.getLongitude();
-               Altitude endAltitude = inEndPoint.getAltitude();
-
                // Loop over points
                for (int i=0; i<inNumPoints; i++)
                {
-                       Coordinate latitude = Coordinate.interpolate(_latitude, endLatitude, i, inNumPoints);
-                       Coordinate longitude = Coordinate.interpolate(_longitude, endLongitude, i, inNumPoints);
-                       Altitude altitude = Altitude.interpolate(_altitude, endAltitude, i, inNumPoints);
+                       Coordinate latitude = Coordinate.interpolate(_latitude, inEndPoint.getLatitude(), i, inNumPoints);
+                       Coordinate longitude = Coordinate.interpolate(_longitude, inEndPoint.getLongitude(), i, inNumPoints);
+                       Altitude altitude = Altitude.interpolate(_altitude, inEndPoint.getAltitude(), i, inNumPoints);
                        range[i] = new DataPoint(latitude, longitude, altitude);
                }
                return range;
        }
 
+       /**
+        * Interpolate between the two given points
+        * @param inStartPoint start point
+        * @param inEndPoint end point
+        * @param inFrac fractional distance from first point (0.0 to 1.0)
+        * @return new DataPoint object between two given ones
+        */
+       public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
+       {
+               if (inStartPoint == null || inEndPoint == null) {return null;}
+               return new DataPoint(
+                       Coordinate.interpolate(inStartPoint.getLatitude(), inEndPoint.getLatitude(), inFrac),
+                       Coordinate.interpolate(inStartPoint.getLongitude(), inEndPoint.getLongitude(), inFrac),
+                       Altitude.interpolate(inStartPoint.getAltitude(), inEndPoint.getAltitude(), inFrac)
+               );
+       }
 
        /**
         * Calculate the number of radians between two points (for distance calculation)
@@ -343,4 +359,14 @@ public class DataPoint
                }
                return false;
        }
+
+
+       /**
+        * Get string for debug
+        * @see java.lang.Object#toString()
+        */
+       public String toString()
+       {
+               return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
+       }
 }
index 5425873102aa752833c9953c91e967705d0ad75b..311cd886a5af098d8764478d4e5aa1d313b08f44 100644 (file)
@@ -8,27 +8,45 @@ public abstract class Distance
        // distance formats
        public static final int UNITS_KILOMETRES = 1;
        public static final int UNITS_MILES      = 2;
+       public static final int UNITS_METRES     = 3;
 
        // Geographical constants
        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 = 0.621371192;
 
 
        /**
         * Convert the given angle in radians into a distance
         * @param inAngDist angular distance in radians
-        * @param inUnits desired units, miles or km
+        * @param inUnits desired units, eg miles or km
         * @return distance in specified format
         */
-       public static double convertRadians(double inAngDist, int inUnits)
+       public static double convertRadiansToDistance(double inAngDist, int inUnits)
        {
                // Multiply by appropriate factor
                if (inUnits == UNITS_MILES)
-                       return inAngDist * EARTH_RADIUS_MILES;
+                       return inAngDist * EARTH_RADIUS_KM * CONVERT_KM_TO_MILES;
+               else if (inUnits == UNITS_METRES)
+                       return inAngDist * EARTH_RADIUS_KM * 1000;
+               // default kilometres
                return inAngDist * EARTH_RADIUS_KM;
        }
 
+       /**
+        * Convert the given distance into an angle in radians
+        * @param inDist distance to convert
+        * @param inUnits units, eg miles or km
+        * @return angular distance in radians
+        */
+       public static double convertDistanceToRadians(double inDist, int inUnits)
+       {
+               // Divide by appropriate factor
+               if (inUnits == UNITS_MILES)
+                       return inDist / EARTH_RADIUS_KM / CONVERT_KM_TO_MILES;
+               else if (inUnits == UNITS_METRES)
+                       return inDist / EARTH_RADIUS_KM / 1000;
+               // default kilometres
+               return inDist / EARTH_RADIUS_KM;
+       }
 }
index 118fae3bab9d161c2ee8989a6bfbe261d5576018..5f28fc578c3c39debbac07fc4944a1615ed5e72d 100644 (file)
@@ -78,6 +78,14 @@ public class Field
                return _builtin;
        }
 
+       /**
+        * @return field type
+        */
+       public FieldType getType()
+       {
+               return _type;
+       }
+
        /**
         * Checks if the two fields are equal
         * @param inOther other Field object
index 5e567ea8512bd80caae67370f0f58734979fabd1..e4baecc97524e6b5b61ac37da47b8fe95ec9ff0e 100644 (file)
@@ -29,7 +29,7 @@ public class FieldList
 
        /**
         * Constructor giving array of Field objects
-        * @param inFieldArray
+        * @param inFieldArray array of Field objects
         */
        public FieldList(Field[] inFieldArray)
        {
index 94ba6861f3055988c8664ce8fb58e1b11a7ef0c3..fca0ed720f1fd512aaed376530bd94c85e2ceb3a 100644 (file)
@@ -6,20 +6,20 @@ package tim.prune.data;
  */
 public class FieldType
 {
-       private int _id = 0;
+       private char _id = 0;
 
-       public static final FieldType NONE =  new FieldType(0);
-       public static final FieldType INT =   new FieldType(1);
-       public static final FieldType BOOL =  new FieldType(2);
-       public static final FieldType COORD = new FieldType(3);
-       public static final FieldType TIME =  new FieldType(4);
+       public static final FieldType NONE =  new FieldType('0');
+       public static final FieldType INT =   new FieldType('1');
+       public static final FieldType BOOL =  new FieldType('2');
+       public static final FieldType COORD = new FieldType('3');
+       public static final FieldType TIME =  new FieldType('4');
 
 
        /**
         * Private constructor
         * @param inId identifier
         */
-       private FieldType(int inId)
+       private FieldType(char inId)
        {
                _id = inId;
        }
index 61e8bb2ebbf17a9245ca1961f3b4f8c11d62ad88..805505054598feab08af8f6f616cfa9bbe8cd234 100644 (file)
@@ -33,8 +33,8 @@ public class Latitude extends Coordinate
        protected int getCardinal(char inChar)
        {
                // Latitude recognises N, S and -
-               // default is North
-               int cardinal = NORTH;
+               // default is No cardinal
+               int cardinal = NO_CARDINAL;
                switch (inChar)
                {
                        case 'N':
@@ -50,6 +50,14 @@ public class Latitude extends Coordinate
                return cardinal;
        }
 
+       /**
+        * @return default cardinal (North)
+        * @see tim.prune.data.Coordinate#getDefaultCardinal()
+        */
+       protected int getDefaultCardinal()
+       {
+               return NORTH;
+       }
 
        /**
         * Make a new Latitude object
@@ -60,4 +68,11 @@ public class Latitude extends Coordinate
                return new Latitude(inValue, inFormat);
        }
 
+       /**
+        * @return the maximum degree range for this coordinate
+        */
+       protected int getMaxDegrees()
+       {
+               return 90;
+       }
 }
index 6af58894aad3d4ba791f1588e9519ea8df16b6cd..b4c04139d5046db1b218b1b52e6019cd2035126c 100644 (file)
@@ -33,8 +33,8 @@ public class Longitude extends Coordinate
        protected int getCardinal(char inChar)
        {
                // Longitude recognises E, W and -
-               // default is East
-               int cardinal = EAST;
+               // default is no cardinal
+               int cardinal = NO_CARDINAL;
                switch (inChar)
                {
                        case 'E':
@@ -51,6 +51,16 @@ public class Longitude extends Coordinate
        }
 
 
+       /**
+        * @return default cardinal (East)
+        * @see tim.prune.data.Coordinate#getDefaultCardinal()
+        */
+       protected int getDefaultCardinal()
+       {
+               return EAST;
+       }
+
+
        /**
         * Make a new Longitude object
         * @see tim.prune.data.Coordinate#makeNew(double, int)
@@ -60,4 +70,11 @@ public class Longitude extends Coordinate
                return new Longitude(inValue, inFormat);
        }
 
+       /**
+        * @return the maximum degree range for this coordinate
+        */
+       protected int getMaxDegrees()
+       {
+               return 180;
+       }
 }
index 3646cb5201ba706d3d2aae30fa021d8407a41adf..2f230cc45dae35db740159724b7bd989002dbfae 100644 (file)
@@ -23,7 +23,8 @@ public class Photo
        /** Current photo status */
        private byte _currentStatus = PhotoStatus.NOT_CONNECTED;
        // TODO: Need to store caption for image?
-       // TODO: Need to store thumbnail for image?
+       // thumbnail for image (from exif)
+       private byte[] _exifThumbnail = null;
 
 
        /**
@@ -171,6 +172,21 @@ public class Photo
                _currentStatus = inStatus;
        }
 
+       /**
+        * @return byte array of thumbnail data
+        */
+       public byte[] getExifThumbnail()
+       {
+               return _exifThumbnail;
+       }
+
+       /**
+        * @param inBytes byte array from exif
+        */
+       public void setExifThumbnail(byte[] inBytes)
+       {
+               _exifThumbnail = inBytes;
+       }
 
        /**
         * Delete the cached data when the Photo is no longer needed
index 4849a4a42bcddb948172bf494f9180d5639eb9a0..b26c11ff7bbca551bbc4f6574dc4123259512fc0 100644 (file)
@@ -1,5 +1,6 @@
 package tim.prune.data;
 
+import tim.prune.DataSubscriber;
 import tim.prune.UpdateMessageBroker;
 
 /**
@@ -243,12 +244,12 @@ public class Selection
 
 
        /**
-        * @param inFormat distance units to use, from class Distance
+        * @param inUnits distance units to use, from class Distance
         * @return distance of Selection in specified units
         */
        public double getDistance(int inUnits)
        {
-               return Distance.convertRadians(_angDistance, inUnits);
+               return Distance.convertRadiansToDistance(_angDistance, inUnits);
        }
 
 
@@ -415,7 +416,6 @@ public class Selection
         */
        public int getCurrentPhotoIndex()
        {
-               // System.out.println("Current photo index = " + _currentPhotoIndex);
                return _currentPhotoIndex;
        }
 
@@ -450,6 +450,6 @@ public class Selection
                                _currentPoint = _startIndex = _endIndex = -1;
                        }
                }
-               _broker.informSubscribers();
+               _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
        }
 }
diff --git a/tim/prune/data/TimeDifference.java b/tim/prune/data/TimeDifference.java
new file mode 100644 (file)
index 0000000..3fee2a1
--- /dev/null
@@ -0,0 +1,123 @@
+package tim.prune.data;
+
+import tim.prune.I18nManager;
+
+/**
+ * Class to represent a time difference, like the difference between two Timestamp objects,
+ * and methods for representing and displaying them.
+ */
+public class TimeDifference
+{
+       private long _totalSeconds = 0L;
+       private int _seconds = 0;
+       private int _minutes = 0;
+       private int _hours = 0;
+       private String _description = null;
+
+
+       /**
+        * Constructor using long
+        * @param inNumSeconds number of seconds time difference
+        */
+       public TimeDifference(long inNumSeconds)
+       {
+               _totalSeconds = inNumSeconds;
+               if (inNumSeconds < 0) {inNumSeconds = -inNumSeconds;}
+               _hours = (int) (inNumSeconds / 60 / 60);
+               _minutes = (int) (inNumSeconds / 60 - _hours * 60);
+               _seconds = (int) (inNumSeconds % 60);
+       }
+
+
+       /**
+        * Constructor giving each field separately
+        * @param inHours number of hours
+        * @param inMinutes number of minutes
+        * @param inSeconds number of seconds
+        * @param inPositive true for positive time difference
+        */
+       public TimeDifference(int inHours, int inMinutes, int inSeconds, boolean inPositive)
+       {
+               // Check for negative values?
+               _hours = inHours;
+               _minutes = inMinutes;
+               _seconds = inSeconds;
+               _totalSeconds = inHours * 3600L + inMinutes * 60L + inSeconds;
+               if (!inPositive) {_totalSeconds = -_totalSeconds;}
+       }
+
+
+       /**
+        * @return total number of seconds time difference
+        */
+       public long getTotalSeconds()
+       {
+               return _totalSeconds;
+       }
+
+       /**
+        * @return number of hours
+        */
+       public int getNumHours()
+       {
+               return _hours;
+       }
+
+       /**
+        * @return number of minutes
+        */
+       public int getNumMinutes()
+       {
+               return _minutes;
+       }
+
+       /**
+        * @return number of seconds
+        */
+       public int getNumSeconds()
+       {
+               return _seconds;
+       }
+
+       /**
+        * @return true if time difference positive
+        */
+       public boolean getIsPositive()
+       {
+               return _totalSeconds >= 0L;
+       }
+
+
+       /**
+        * Build a String to describe the time duration
+        * @return time as a string, days, hours, mins, secs as appropriate
+        */
+       public String getDescription()
+       {
+               if (_description != null) {return _description;}
+               StringBuffer buffer = new StringBuffer();
+               boolean started = false;
+               // hours
+               if (_hours > 0)
+               {
+                       buffer.append(_hours).append(' ').append(I18nManager.getText("display.range.time.hours"));
+                       started = true;
+               }
+               // minutes
+               if (_minutes > 0)
+               {
+                       if (started) {buffer.append(", ");}
+                       else {started = true;}
+                       buffer.append(_minutes).append(' ').append(I18nManager.getText("display.range.time.mins"));
+               }
+               // seconds
+               if (_seconds > 0 || !started)
+               {
+                       if (started) {buffer.append(", ");}
+                       buffer.append(_seconds).append(' ').append(I18nManager.getText("display.range.time.secs"));
+               }
+               _description = buffer.toString();
+               return _description;
+       }
+
+}
index ddb380818fc45d091c92ccd57111b5ae40d34eee..334bf499e94729ca32d18fdccaaf41c429a5708f 100644 (file)
@@ -15,8 +15,11 @@ public class Timestamp
        private boolean _valid = false;
        private long _seconds = 0L;
        private String _text = null;
+       private String _timeText = null;
 
        private static DateFormat DEFAULT_DATE_FORMAT = DateFormat.getDateTimeInstance();
+       private static DateFormat DEFAULT_TIME_FORMAT = DateFormat.getTimeInstance();
+       private static DateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        private static DateFormat[] ALL_DATE_FORMATS = null;
        private static Calendar CALENDAR = null;
        private static long SECS_SINCE_1970 = 0L;
@@ -24,9 +27,15 @@ public class Timestamp
        private static long MSECS_SINCE_1970 = 0L;
        private static long MSECS_SINCE_1990 = 0L;
        private static long TWENTY_YEARS_IN_SECS = 0L;
-
        private static final long GARTRIP_OFFSET = 631065600L;
 
+       /** Specifies original timestamp format */
+       public static final int FORMAT_ORIGINAL = 0;
+       /** Specifies locale-dependent timestamp format */
+       public static final int FORMAT_LOCALE = 1;
+       /** Specifies ISO 8601 timestamp format */
+       public static final int FORMAT_ISO_8601 = 2;
+
        // Static block to initialise offsets
        static
        {
@@ -43,13 +52,15 @@ public class Timestamp
                        new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy"),
                        new SimpleDateFormat("HH:mm:ss dd MMM yyyy"),
                        new SimpleDateFormat("dd MMM yyyy HH:mm:ss"),
-                       new SimpleDateFormat("yyyy MMM dd HH:mm:ss")
+                       new SimpleDateFormat("yyyy MMM dd HH:mm:ss"),
+                       ISO_8601_FORMAT
                };
        }
 
 
        /**
         * Constructor
+        * @param inString String containing timestamp
         */
        public Timestamp(String inString)
        {
@@ -83,10 +94,10 @@ public class Timestamp
                                        _seconds = rawValue / 1000L + TWENTY_YEARS_IN_SECS;
                                        smallestDiff = diff3;
                                }
-                               // Lastly, check garmin offset
+                               // Lastly, check gartrip offset
                                if (diff4 < smallestDiff)
                                {
-                                       // seconds since garmin offset
+                                       // seconds since gartrip offset
                                        _seconds = rawValue + GARTRIP_OFFSET;
                                }
                                _valid = true;
@@ -137,8 +148,8 @@ public class Timestamp
 
 
        /**
-        * Constructor giving millis since 1970
-        * @param inMillis
+        * Constructor giving millis
+        * @param inMillis milliseconds since 1970
         */
        public Timestamp(long inMillis)
        {
@@ -167,20 +178,88 @@ public class Timestamp
        }
 
 
+       /**
+        * Add the given TimeDifference to this Timestamp
+        * @param inOffset TimeDifference to add
+        * @return new Timestamp object
+        */
+       public Timestamp addOffset(TimeDifference inOffset)
+       {
+               return new Timestamp((_seconds + inOffset.getTotalSeconds()) * 1000L);
+       }
+
+
+       /**
+        * Subtract the given TimeDifference from this Timestamp
+        * @param inOffset TimeDifference to subtract
+        * @return new Timestamp object
+        */
+       public Timestamp subtractOffset(TimeDifference inOffset)
+       {
+               return new Timestamp((_seconds - inOffset.getTotalSeconds()) * 1000L);
+       }
+
+
        /**
         * @return Description of timestamp in locale-specific format
         */
        public String getText()
        {
+               return getText(FORMAT_LOCALE);
+       }
+
+       /**
+        * @param inFormat format of timestamp
+        * @return Description of timestamp in required format
+        */
+       public String getText(int inFormat)
+       {
+               if (inFormat == FORMAT_ISO_8601) {
+                       return format(ISO_8601_FORMAT);
+               }
                if (_text == null)
                {
-                       if (_valid)
-                       {
-                               CALENDAR.setTimeInMillis(_seconds * 1000L);
-                               _text = DEFAULT_DATE_FORMAT.format(CALENDAR.getTime());
+                       if (_valid) {
+                               _text = format(DEFAULT_DATE_FORMAT);
                        }
                        else _text = "";
                }
                return _text;
        }
+
+       /**
+        * @return Description of time part of timestamp in locale-specific format
+        */
+       public String getTimeText()
+       {
+               if (_timeText == null)
+               {
+                       if (_valid) {
+                               _timeText = format(DEFAULT_TIME_FORMAT);
+                       }
+                       else _timeText = "";
+               }
+               return _timeText;
+       }
+
+       /**
+        * Utility method for formatting dates / times
+        * @param inFormat formatter object
+        * @return formatted String
+        */
+       private String format(DateFormat inFormat)
+       {
+               CALENDAR.setTimeInMillis(_seconds * 1000L);
+               return inFormat.format(CALENDAR.getTime());
+       }
+
+       /**
+        * @return a Calendar object representing this timestamp
+        */
+       public Calendar getCalendar()
+       {
+               Calendar cal = Calendar.getInstance();
+               cal.setTimeInMillis(_seconds * 1000L);
+               return cal;
+       }
 }
index c5a0f16f4d72a3707ebf4460088958c97cbf14c0..1188a7c5a83d6edef42a2d8f66c1b7bf59f608df 100644 (file)
@@ -32,9 +32,8 @@ public class Track
 
 
        /**
-        * Constructor giving arrays of Fields and Objects
-        * @param inFieldArray field array
-        * @param inPointArray 2d array of field values
+        * Constructor for empty track
+        * @param inBroker message broker object
         */
        public Track(UpdateMessageBroker inBroker)
        {
@@ -182,7 +181,6 @@ public class Track
                        System.arraycopy(newPointArray, 0, _dataPoints, 0, numCopied);
                        _numPoints = _dataPoints.length;
                        _scaled = false;
-                       _broker.informSubscribers();
                }
                return numDeleted;
        }
@@ -212,6 +210,7 @@ public class Track
 
        /**
         * Delete the specified point
+        * @param inIndex point index
         * @return true if successful
         */
        public boolean deletePoint(int inIndex)
@@ -298,7 +297,6 @@ public class Track
                        _dataPoints = newPointArray;
                        _numPoints = _dataPoints.length;
                        _scaled = false;
-                       _broker.informSubscribers();
                }
                return numDupes;
        }
@@ -306,6 +304,8 @@ public class Track
 
        /**
         * Reverse the specified range of points
+        * @param inStart start index
+        * @param inEnd end index
         * @return true if successful, false otherwise
         */
        public boolean reverseRange(int inStart, int inEnd)
@@ -848,6 +848,7 @@ public class Track
        /**
         * Replace the track contents with the given point array
         * @param inContents array of DataPoint objects
+        * @return true on success
         */
        public boolean replaceContents(DataPoint[] inContents)
        {
index b987ea9449d6dff66fd52f2af60031752f415413..f8e3c844b8ac86bc46c7c45bf8b78c7bc976f88e 100644 (file)
@@ -1,7 +1,7 @@
 package tim.prune.data;
 
-import java.util.List;
-
+import java.util.Iterator;
+import java.util.Set;
 import tim.prune.UpdateMessageBroker;
 
 /**
@@ -100,23 +100,24 @@ public class TrackInfo
 
 
        /**
-        * Add a List of Photos
-        * @param inList List containing Photo objects
+        * Add a Set of Photos
+        * @param inSet Set containing Photo objects
         * @return array containing number of photos and number of points added
         */
-       public int[] addPhotos(List inList)
+       public int[] addPhotos(Set inSet)
        {
-               // 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())
+               Iterator iterator = null;
+               if (inSet != null && !inSet.isEmpty())
                {
-                       for (int i=0; i<inList.size(); i++)
+                       iterator = inSet.iterator();
+                       while (iterator.hasNext())
                        {
                                try
                                {
-                                       Photo photo = (Photo) inList.get(i);
+                                       Photo photo = (Photo) iterator.next();
                                        if (photo != null && !_photoList.contains(photo))
                                        {
                                                numPhotosToAdd++;
@@ -136,11 +137,12 @@ public class TrackInfo
                        int pointNum = 0;
                        boolean hasAltitude = false;
                        // Add each Photo in turn
-                       for (int i=0; i<inList.size(); i++)
+                       iterator = inSet.iterator();
+                       while (iterator.hasNext())
                        {
                                try
                                {
-                                       Photo photo = (Photo) inList.get(i);
+                                       Photo photo = (Photo) iterator.next();
                                        if (photo != null && !_photoList.contains(photo))
                                        {
                                                // Add photo
@@ -248,8 +250,10 @@ public class TrackInfo
        public int compress(int inResolution)
        {
                int numDeleted = _track.compress(inResolution);
-               if (numDeleted > 0)
+               if (numDeleted > 0) {
                        _selection.clearAll();
+                       _broker.informSubscribers();
+               }
                return numDeleted;
        }
 
@@ -261,8 +265,10 @@ public class TrackInfo
        public int deleteDuplicates()
        {
                int numDeleted = _track.deleteDuplicates();
-               if (numDeleted > 0)
+               if (numDeleted > 0) {
                        _selection.clearAll();
+                       _broker.informSubscribers();
+               }
                return numDeleted;
        }
 
index 4e58199f0aab1bd4c5d1fccc6a164e1c114c9196..1637a9dbc633639d4126c2e892f58ce9f7e03498 100644 (file)
@@ -20,6 +20,11 @@ public class ExifReader
         */\r
        private boolean _isMotorolaByteOrder;\r
 \r
+       /** Thumbnail offset */\r
+       private int _thumbnailOffset = -1;\r
+       /** Thumbnail length */\r
+       private int _thumbnailLength = -1;\r
+\r
        /**\r
         * The number of bytes used per format descriptor.\r
         */\r
@@ -71,11 +76,18 @@ public class ExifReader
        public static final int TAG_GPS_TIMESTAMP = 0x0007;\r
        /** GPS date (atomic clock) GPSDateStamp 23 1d RATIONAL 3 */\r
        public static final int TAG_GPS_DATESTAMP = 0x001d;\r
+       /** Exif timestamp */\r
+       public static final int TAG_DATETIME_ORIGINAL = 0x9003;\r
+       /** Thumbnail offset */\r
+       private static final int TAG_THUMBNAIL_OFFSET = 0x0201;\r
+       /** Thumbnail length */\r
+       private static final int TAG_THUMBNAIL_LENGTH = 0x0202;\r
+\r
 \r
        /**\r
         * Creates an ExifReader for a Jpeg file.\r
         * @param inFile File object to attempt to read from\r
-        * @throws JpegProcessingException on failure\r
+        * @throws JpegException on failure\r
         */\r
        public ExifReader(File inFile) throws JpegException\r
        {\r
@@ -238,11 +250,11 @@ public class ExifReader
                        // Calculate the value as an offset for cases where the tag represents a directory\r
                        final int subdirOffset = inTiffHeaderOffset + get32Bits(tagValueOffset);\r
 \r
-                       // TODO: Also look for timestamp(s) in Exif for correlation - which directory?\r
+                       // Look in both basic Exif tags (for timestamp, thumbnail) and Gps tags (for lat, long, altitude, timestamp)\r
                        switch (tagType)\r
                        {\r
                                case TAG_EXIF_OFFSET:\r
-                                       // ignore\r
+                                       processDirectory(inMetadata, false, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset);\r
                                        continue;\r
                                case TAG_INTEROP_OFFSET:\r
                                        // ignore\r
@@ -255,9 +267,14 @@ public class ExifReader
                                        continue;\r
                                default:\r
                                        // not a known directory, so must just be a normal tag\r
-                                       // ignore if we're not in gps directory\r
                                        if (inIsGPS)\r
+                                       {\r
                                                processGpsTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
+                                       }\r
+                                       else\r
+                                       {\r
+                                               processExifTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
+                                       }\r
                                        break;\r
                        }\r
                }\r
@@ -336,16 +353,56 @@ public class ExifReader
                                inMetadata.setAltitude(readRational(inTagValueOffset, inFormatCode, inComponentCount));\r
                                break;\r
                        case TAG_GPS_TIMESTAMP:\r
-                               inMetadata.setTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
+                               inMetadata.setGpsTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
                                break;\r
                        case TAG_GPS_DATESTAMP:\r
-                               inMetadata.setDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
+                               inMetadata.setGpsDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
                                break;\r
                        default: // ignore all other tags\r
                }\r
        }\r
 \r
 \r
+       /**\r
+        * Process a general Exif tag\r
+        * @param inMetadata metadata holding extracted values\r
+        * @param inTagType tag type (eg latitude)\r
+        * @param inTagValueOffset start offset in data array\r
+        * @param inComponentCount component count for tag\r
+        * @param inFormatCode format code, eg byte\r
+        */\r
+       private void processExifTag(JpegData inMetadata, int inTagType, int inTagValueOffset,\r
+               int inComponentCount, int inFormatCode)\r
+       {\r
+               // Only interested in original timestamp, thumbnail offset and thumbnail length\r
+               if (inTagType == TAG_DATETIME_ORIGINAL)\r
+               {\r
+                       inMetadata.setOriginalTimestamp(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
+               }\r
+               else if (inTagType == TAG_THUMBNAIL_OFFSET) {\r
+                       _thumbnailOffset = TIFF_HEADER_START_OFFSET + get16Bits(inTagValueOffset);\r
+                       extractThumbnail(inMetadata);\r
+               }\r
+               else if (inTagType == TAG_THUMBNAIL_LENGTH) {\r
+                       _thumbnailLength = get16Bits(inTagValueOffset);\r
+                       extractThumbnail(inMetadata);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Attempt to extract the thumbnail image\r
+        */\r
+       private void extractThumbnail(JpegData inMetadata)\r
+       {\r
+               if (_thumbnailOffset > 0 && _thumbnailLength > 0 && inMetadata.getThumbnailImage() == null)\r
+               {\r
+                       byte[] thumbnailBytes = new byte[_thumbnailLength];\r
+                       System.arraycopy(_data, _thumbnailOffset, thumbnailBytes, 0, _thumbnailLength);\r
+                       inMetadata.setThumbnailImage(thumbnailBytes);\r
+               }\r
+       }\r
+\r
+\r
        /**\r
         * Calculate the tag value offset\r
         * @param inByteCount\r
index 51995c96922b90f30ad117f6394f2e1b0424c758..3d16733392d8c394d4e33f3725cd108ef74bea38 100644 (file)
@@ -16,8 +16,10 @@ public class JpegData
        private Rational[] _latitude = null;
        private Rational[] _longitude = null;
        private Rational   _altitude = null;
-       private Rational[] _timestamp = null;
-       private Rational[] _datestamp = null;
+       private Rational[] _gpsTimestamp = null;
+       private Rational[] _gpsDatestamp = null;
+       private String _originalTimestamp = null;
+       private byte[] _thumbnail = null;
        private ArrayList _errors = null;
 
 
@@ -113,21 +115,30 @@ public class JpegData
        }
 
        /**
-        * Set the timestamp
+        * Set the Gps timestamp
         * @param inValues array of Rationals holding timestamp
         */
-       public void setTimestamp(Rational[] inValues)
+       public void setGpsTimestamp(Rational[] inValues)
        {
-               _timestamp = inValues;
+               _gpsTimestamp = inValues;
        }
 
        /**
-        * Set the datestamp
+        * Set the Gps datestamp
         * @param inValues array of Rationals holding datestamp
         */
-       public void setDatestamp(Rational[] inValues)
+       public void setGpsDatestamp(Rational[] inValues)
        {
-               _datestamp = inValues;
+               _gpsDatestamp = inValues;
+       }
+
+       /**
+        * Set the original timestamp
+        * @param inStamp original timestamp of photo
+        */
+       public void setOriginalTimestamp(String inStamp)
+       {
+               _originalTimestamp = inStamp;
        }
 
        /** @return latitude ref as char */
@@ -142,10 +153,24 @@ public class JpegData
        public byte getAltitudeRef() { return _altitudeRef; }
        /** @return altitude as Rational */
        public Rational getAltitude() { return _altitude; }
-       /** @return timestamp as array of 3 Rationals */
-       public Rational[] getTimestamp() { return _timestamp; }
-       /** @return timestamp as array of 3 Rationals */
-       public Rational[] getDatestamp() { return _datestamp; }
+       /** @return Gps timestamp as array of 3 Rationals */
+       public Rational[] getGpsTimestamp() { return _gpsTimestamp; }
+       /** @return Gps datestamp as array of 3 Rationals */
+       public Rational[] getGpsDatestamp() { return _gpsDatestamp; }
+       /** @return original timestamp as string */
+       public String getOriginalTimestamp() { return _originalTimestamp; }
+
+       /**
+        * Set the thumbnail
+        * @param inBytes byte array containing thumbnail
+        */
+       public void setThumbnailImage(byte[] inBytes) {
+               _thumbnail = inBytes;
+       }
+       /** @return thumbnail as byte array */
+       public byte[] getThumbnailImage() {
+               return _thumbnail;
+       }
 
        /**
         * @return true if data looks valid, ie has at least lat and long
index 6a0d6a444b609c22e393cb36533d347892cab7c4..23197f8c1fb105f8e6dc0068f60e3f67110dcfc1 100644 (file)
@@ -30,7 +30,6 @@ public class JpegSegmentData
         */\r
        public void addSegment(byte inSegmentMarker, byte[] inSegmentBytes)\r
        {\r
-               // System.out.println("Adding segment: " + inSegmentMarker);\r
                List segmentList = getOrCreateSegmentList(inSegmentMarker);\r
                segmentList.add(inSegmentBytes);\r
        }\r
index 78bd72b3d281bf62135e4c5a6f53f6c87e01ebe5..e522935159473d673b0cdf55be642e4bd78078d9 100644 (file)
@@ -62,7 +62,8 @@ public class Rational
        }\r
 \r
        /**\r
-        * Checks if this rational number is an Integer, either positive or negative.\r
+        * Checks if this rational number is an Integer, either positive or negative\r
+        * @return true if an integer\r
         */\r
        public boolean isInteger()\r
        {\r
index 575b0aa9ba90e2161ab91840e3b2a8204c918c58..fb4a86bf77d9cd0f04f78f50dc6ada65a234a7bf 100644 (file)
@@ -16,6 +16,7 @@ public class EditFieldsTableModel extends AbstractTableModel
 
        /**
         * Constructor giving list size
+        * @param inSize number of fields
         */
        public EditFieldsTableModel(int inSize)
        {
@@ -70,7 +71,7 @@ public class EditFieldsTableModel extends AbstractTableModel
                {
                        return _fieldValues[inRowIndex];
                }
-               return new Boolean(_valueChanged[inRowIndex]);
+               return Boolean.valueOf(_valueChanged[inRowIndex]);
        }
 
 
index 1e4f4f5bf5f98f04693f6657d5f460b29999fcc2..605abf6dff2c3ea62eff1330adc262d31fdd54cc 100644 (file)
@@ -35,6 +35,7 @@ public class AboutScreen extends JDialog
 
        /**
         * Constructor
+        * @param inParent parent frame
         */
        public AboutScreen(JFrame inParent)
        {
@@ -147,29 +148,35 @@ public class AboutScreen extends JDialog
                        new JLabel("Eclipse"),
                        1, 2);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "),
+                       new JLabel(I18nManager.getText("dialog.about.credits.translators") + " : "),
                        0, 3);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel("Open Office, Gpsdrive, Babelfish, Leo"),
+                       new JLabel("Ramon, Miguel, Inés, Piotr"),
                        1, 3);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "),
+                       new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "),
                        0, 4);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"),
+                       new JLabel("Open Office, Gpsdrive, Babelfish, Leo, Launchpad"),
                        1, 4);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "),
+                       new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "),
                        0, 5);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel("Garble, Kate, Povray, Inkscape, Google Earth"),
+                       new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"),
                        1, 5);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "),
+                       new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "),
                        0, 6);
                addToGridBagPanel(creditsPanel, gridBag, constraints,
-                       new JLabel("Friends and loved ones, for encouragement and support"),
+                       new JLabel("Garble, Kate, Povray, Exiftool, Inkscape, Google Earth"),
                        1, 6);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "),
+                       0, 7);
+               addToGridBagPanel(creditsPanel, gridBag, constraints,
+                       new JLabel("Friends and loved ones, for encouragement and support"),
+                       1, 7);
                tabPane.add(I18nManager.getText("dialog.about.credits"), creditsPanel);
 
                // OK button at the bottom
index 1c686262dff5b979a01ab4d1a590efe8546fc54d..aab8362e4d47e5328ba39d224e404857d2387793 100644 (file)
@@ -23,6 +23,7 @@ import tim.prune.data.DataPoint;
 import tim.prune.data.Distance;
 import tim.prune.data.IntegerRange;
 import tim.prune.data.Photo;
+import tim.prune.data.PhotoStatus;
 import tim.prune.data.Selection;
 import tim.prune.data.TrackInfo;
 
@@ -46,9 +47,11 @@ public class DetailsDisplay extends GenericDisplay
        // Photo details
        private JLabel _photoLabel = null;
        private PhotoThumbnail _photoThumbnail = null;
+       private JLabel _photoConnectedLabel = null;
 
        // Units
-       private JComboBox _unitsDropdown = null;
+       private JComboBox _coordFormatDropdown = null;
+       private JComboBox _distUnitsDropdown = null;
        // Formatter
        private NumberFormat _distanceFormatter = NumberFormat.getInstance();
 
@@ -139,6 +142,8 @@ public class DetailsDisplay extends GenericDisplay
                photoDetailsPanel.add(photoDetailsLabel);
                _photoLabel = new JLabel(I18nManager.getText("details.nophoto"));
                photoDetailsPanel.add(_photoLabel);
+               _photoConnectedLabel = new JLabel("");
+               photoDetailsPanel.add(_photoConnectedLabel);
                _photoThumbnail = new PhotoThumbnail();
                _photoThumbnail.setVisible(false);
                _photoThumbnail.setPreferredSize(new Dimension(100, 100));
@@ -154,28 +159,43 @@ public class DetailsDisplay extends GenericDisplay
                // add the main panel at the top
                add(mainPanel, BorderLayout.NORTH);
 
-               // Add units selection
+               // Add format, units selection
                JPanel lowerPanel = new JPanel();
                lowerPanel.setLayout(new BoxLayout(lowerPanel, BoxLayout.Y_AXIS));
+               JLabel coordFormatLabel = new JLabel(I18nManager.getText("details.coordformat") + ": ");
+               coordFormatLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+               lowerPanel.add(coordFormatLabel);
+               String[] coordFormats = {I18nManager.getText("units.original"), I18nManager.getText("units.degminsec"),
+                       I18nManager.getText("units.degmin"), I18nManager.getText("units.deg")};
+               _coordFormatDropdown = new JComboBox(coordFormats);
+               _coordFormatDropdown.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               dataUpdated(DataSubscriber.UNITS_CHANGED);
+                       }
+               });
+               lowerPanel.add(_coordFormatDropdown);
+               _coordFormatDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
                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() {
+               _distUnitsDropdown = new JComboBox(distUnits);
+               _distUnitsDropdown.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
                                dataUpdated(DataSubscriber.UNITS_CHANGED);
                        }
                });
-               lowerPanel.add(_unitsDropdown);
-               _unitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
+               lowerPanel.add(_distUnitsDropdown);
+               _distUnitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
                add(lowerPanel, BorderLayout.SOUTH);
        }
 
 
        /**
         * Notification that Track has been updated
+        * @param inUpdateType byte to specify what has been updated
         */
        public void dataUpdated(byte inUpdateType)
        {
@@ -197,8 +217,8 @@ public class DetailsDisplay extends GenericDisplay
                        _indexLabel.setText(LABEL_POINT_SELECTED1
                                + (currentPointIndex+1) + " " + I18nManager.getText("details.index.of")
                                + " " + _track.getNumPoints());
-                       _latLabel.setText(LABEL_POINT_LATITUDE + currentPoint.getLatitude().output(Coordinate.FORMAT_NONE));
-                       _longLabel.setText(LABEL_POINT_LONGITUDE + currentPoint.getLongitude().output(Coordinate.FORMAT_NONE));
+                       _latLabel.setText(makeCoordinateLabel(LABEL_POINT_LATITUDE, currentPoint.getLatitude(), _coordFormatDropdown.getSelectedIndex()));
+                       _longLabel.setText(makeCoordinateLabel(LABEL_POINT_LONGITUDE, currentPoint.getLongitude(), _coordFormatDropdown.getSelectedIndex()));
                        _altLabel.setText(LABEL_POINT_ALTITUDE
                                + (currentPoint.hasAltitude()?
                                        (currentPoint.getAltitude().getValue() + getAltitudeUnitsLabel(currentPoint.getAltitude().getFormat())):
@@ -229,7 +249,7 @@ public class DetailsDisplay extends GenericDisplay
                        _rangeLabel.setText(LABEL_RANGE_SELECTED1
                                + (selection.getStart()+1) + " " + I18nManager.getText("details.range.to")
                                + " " + (selection.getEnd()+1));
-                       if (_unitsDropdown.getSelectedIndex() == 0)
+                       if (_distUnitsDropdown.getSelectedIndex() == 0)
                                _distanceLabel.setText(LABEL_RANGE_DISTANCE + buildDistanceString(
                                        selection.getDistance(Distance.UNITS_KILOMETRES))
                                        + " " + I18nManager.getText("units.kilometres.short"));
@@ -264,12 +284,16 @@ public class DetailsDisplay extends GenericDisplay
                {
                        // no photo, hide details
                        _photoLabel.setText(I18nManager.getText("details.nophoto"));
+                       _photoConnectedLabel.setText("");
                        _photoThumbnail.setVisible(false);
                }
                else
                {
                        if (currentPhoto == null) {currentPhoto = currentPoint.getPhoto();}
                        _photoLabel.setText(I18nManager.getText("details.photofile") + ": " + currentPhoto.getFile().getName());
+                       _photoConnectedLabel.setText(I18nManager.getText("details.photo.connected") + ": "
+                               + (currentPhoto.getCurrentStatus() == PhotoStatus.NOT_CONNECTED ?
+                                       I18nManager.getText("dialog.about.no"):I18nManager.getText("dialog.about.yes")));
                        _photoThumbnail.setVisible(true);
                        _photoThumbnail.setPhoto(currentPhoto);
                }
@@ -293,6 +317,30 @@ public class DetailsDisplay extends GenericDisplay
        }
 
 
+       /**
+        * Construct an appropriate coordinate label using the selected format
+        * @param inPrefix prefix of label
+        * @param inCoordinate coordinate
+        * @param inFormat index of format selection dropdown
+        * @return language-sensitive string
+        */
+       private static String makeCoordinateLabel(String inPrefix, Coordinate inCoordinate, int inFormat)
+       {
+               String coord = null;
+               switch (inFormat) {
+                       case 1: // degminsec
+                               coord = inCoordinate.output(Coordinate.FORMAT_DEG_MIN_SEC); break;
+                       case 2: // degmin
+                               coord = inCoordinate.output(Coordinate.FORMAT_DEG_MIN); break;
+                       case 3: // degrees
+                               coord = inCoordinate.output(Coordinate.FORMAT_DEG); break;
+                       default: // just as it was
+                               coord = inCoordinate.output(Coordinate.FORMAT_NONE);
+               }
+               return inPrefix + coord;
+       }
+
+
        /**
         * Build a String to describe a time duration
         * @param inNumSecs number of seconds
index e705f843aab3c74eb688f173766d5bbb5e7c8820..a22cb1811b9bc513c135173011db478e24ea2ef8 100644 (file)
@@ -49,6 +49,7 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
        private BufferedImage _image = null;
        private JPopupMenu _popup = null;
        private JCheckBoxMenuItem _autoPanMenuItem = null;
+       private JCheckBoxMenuItem _connectPointsMenuItem = null;
        private int _numPoints = -1;
        private double _scale;
        private double _offsetX, _offsetY, _zoomScale;
@@ -97,12 +98,13 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
 
        /**
         * Override paint method to draw map
+        * @param inG graphics object
         */
-       public void paint(Graphics g)
+       public void paint(Graphics inG)
        {
                if (_track == null)
                {
-                       super.paint(g);
+                       super.paint(inG);
                        return;
                }
 
@@ -161,14 +163,14 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                if (_image == null) {return;}
 
                // draw buffered image onto g
-               g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
+               inG.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
 
                // draw selected range, if any
                if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
                {
                        int rangeStart = _trackInfo.getSelection().getStart();
                        int rangeEnd = _trackInfo.getSelection().getEnd();
-                       g.setColor(COLOR_CURR_RANGE);
+                       inG.setColor(COLOR_CURR_RANGE);
                        for (int i=rangeStart; i<=rangeEnd; i++)
                        {
                                x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
@@ -176,7 +178,7 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                                if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
                                        && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
                                {
-                                       g.drawOval(x - 2, y - 2, 4, 4);
+                                       inG.drawRect(x - 2, y - 2, 4, 4);
                                }
                        }
                }
@@ -184,47 +186,51 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                // Highlight selected point
                if (selectedPoint >= 0 && !_zoomDragging)
                {
-                       g.setColor(COLOR_CROSSHAIRS);
+                       inG.setColor(COLOR_CROSSHAIRS);
                        x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
                        y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
                        if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
                                && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
                        {
                                // Draw cross-hairs for current point
-                               g.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
-                               g.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
+                               inG.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
+                               inG.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
 
                                // Show selected point afterwards to make sure it's on top
-                               g.drawOval(x - 2, y - 2, 4, 4);
-                               g.drawOval(x - 3, y - 3, 6, 6);
+                               inG.drawOval(x - 2, y - 2, 4, 4);
+                               inG.drawOval(x - 3, y - 3, 6, 6);
                        }
                }
 
                // Draw rectangle for dragging zoom area
                if (_zoomDragging)
                {
-                       g.setColor(COLOR_CROSSHAIRS);
-                       g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
-                       g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
-                       g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
-                       g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
+                       inG.setColor(COLOR_CROSSHAIRS);
+                       inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
+                       inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
+                       inG.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
+                       inG.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
                }
 
                // Attempt to grab keyboard focus if possible
-               //this.requestFocus();
+               //requestFocus();  (causes problems here)
        }
 
 
        /**
-        * Draw the map onto an offscreen image
+        * Plot the points onto an offscreen image
+        * which doesn't have to be redrawn when the selection changes
         */
        private void createBackgroundImage()
        {
                int width = getWidth();
                int height = getHeight();
                int x, y;
-               // Make a new image and initialise it
-               _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+               int lastX = 0, lastY = 0;
+               // Initialise image
+               if (_image == null || _image.getWidth() != width || _image.getHeight() != height) {
+                       _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+               }
                Graphics bufferedG = _image.getGraphics();
                super.paint(bufferedG);
 
@@ -233,6 +239,7 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                bufferedG.setColor(COLOR_POINT);
                int halfWidth = width/2;
                int halfHeight = height/2;
+               boolean currPointTrackpoint = false, lastPointTrackpoint = false;
                for (int i=0; i<numPoints; i++)
                {
                        x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
@@ -240,8 +247,21 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                        if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
                                && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
                        {
-                               bufferedG.drawOval(x - 2, y - 2, 4, 4);
+                               // draw block for point (a bit faster than circles)
+                               bufferedG.drawRect(x - 2, y - 2, 3, 3);
+
+                               // See whether to connect the point with previous one or not
+                               currPointTrackpoint = !_track.getPoint(i).isWaypoint() && _track.getPoint(i).getPhoto() == null;
+                               if (_connectPointsMenuItem.isSelected() && currPointTrackpoint && lastPointTrackpoint)
+                               {
+                                       bufferedG.drawLine(lastX, lastY, x, y);
+                               }
+                               lastPointTrackpoint = currPointTrackpoint;
+                       }
+                       else {
+                               lastPointTrackpoint = false;
                        }
+                       lastX = x; lastY = y;
                }
 
                // Loop again and show waypoints with names
@@ -268,36 +288,34 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                                        int nameWidth = fm.stringWidth(waypointName);
                                        if (nameWidth < (width - 2 * BORDER_WIDTH))
                                        {
-                                               double nameAngle = 0.3;
-                                               double nameRadius = 1.0;
                                                boolean drawnName = false;
-                                               while (!drawnName)
+                                               // Make arrays for coordinates right left up down
+                                               int[] nameXs = {x + 2, x - nameWidth - 2, x - nameWidth/2, x - nameWidth/2};
+                                               int[] nameYs = {y + (nameHeight/2), y + (nameHeight/2), y - 2, y + nameHeight + 2};
+                                               for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
                                                {
-                                                       int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2);
-                                                       int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2);
-                                                       if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH)
-                                                               && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH)
+                                                       // Shift arrays for coordinates right left up down
+                                                       nameXs[0] += 2; nameXs[1] -= 2;
+                                                       nameYs[2] -= 2; nameYs[3] += 2;
+                                                       // Check each direction in turn right left up down
+                                                       for (int a=0; a<4; a++)
                                                        {
-                                                               // name can fit in grid - does it overlap data points?
-                                                               if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
+                                                               if (nameXs[a] > BORDER_WIDTH && (nameXs[a] + nameWidth) < (width - BORDER_WIDTH)
+                                                                       && nameYs[a] < (height - BORDER_WIDTH) && (nameYs[a] - nameHeight) > BORDER_WIDTH
+                                                                       && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight))
                                                                {
-                                                                       bufferedG.drawString(waypointName, nameX, nameY);
+                                                                       // Found a rectangle to fit - draw name here and quit
+                                                                       bufferedG.drawString(waypointName, nameXs[a], nameYs[a]);
                                                                        drawnName = true;
-                                                                       numWaypointNamesShown++;
+                                                                       break;
                                                                }
                                                        }
-                                                       nameAngle += 0.08;
-                                                       nameRadius += 0.2;
-                                                       // wasn't room within the radius, so don't print name
-                                                       if (nameRadius > 50.0)
-                                                       {
-                                                               drawnName = true;
-                                                       }
                                                }
                                        }
                                }
                        }
                }
+               bufferedG.dispose();
        }
 
 
@@ -361,6 +379,16 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
                        }});
                zoomFull.setEnabled(true);
                _popup.add(zoomFull);
+               _connectPointsMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.connect"));
+               _connectPointsMenuItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               // redraw map
+                               dataUpdated(DataSubscriber.ALL);
+                       }
+               });
+               _connectPointsMenuItem.setSelected(false);
+               _popup.add(_connectPointsMenuItem);
                _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
                _autoPanMenuItem.setSelected(true);
                _popup.add(_autoPanMenuItem);
@@ -421,24 +449,25 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis
 
        /**
         * React to click on map display
+        * @param inE mouse event
         */
-       public void mouseClicked(MouseEvent e)
+       public void mouseClicked(MouseEvent inE)
        {
                this.requestFocus();
                if (_track != null)
                {
-                       int xClick = e.getX();
-                       int yClick = e.getY();
+                       int xClick = inE.getX();
+                       int yClick = inE.getY();
                        // Check click is within main area (not in border)
                        if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
                                && yClick < (getHeight() - BORDER_WIDTH))
                        {
                                // Check left click or right click
-                               if (e.isMetaDown())
+                               if (inE.isMetaDown())
                                {
                                        // Only show popup if track has data
                                        if (_track != null && _track.getNumPoints() > 0)
-                                               _popup.show(this, e.getX(), e.getY());
+                                               _popup.show(this, xClick, yClick);
                                }
                                else
                                {
index 5a4b009ada4d092265b337cc72234fd76ad934e4..b97bb76bf660b0f72bf43c6fef948624c898f8f6 100644 (file)
@@ -37,6 +37,7 @@ public class MenuManager implements DataSubscriber
        // Menu items which need enabling/disabling
        private JMenuItem _saveItem = null;
        private JMenuItem _exportKmlItem = null;
+       private JMenuItem _exportGpxItem = null;
        private JMenuItem _exportPovItem = null;
        private JMenuItem _undoItem = null;
        private JMenuItem _clearUndoItem = null;
@@ -60,7 +61,8 @@ public class MenuManager implements DataSubscriber
        private JMenuItem _saveExifItem = null;
        private JMenuItem _connectPhotoItem = null;
        private JMenuItem _deletePhotoItem = null;
-       // TODO: Does Photo menu require disconnect option?
+       private JMenuItem _disconnectPhotoItem = null;
+       private JMenuItem _correlatePhotosItem = null;
 
        // ActionListeners for reuse by menu and toolbar
        private ActionListener _openFileAction = null;
@@ -85,6 +87,7 @@ public class MenuManager implements DataSubscriber
         * Constructor
         * @param inParent parent object for dialogs
         * @param inApp application to call on menu actions
+        * @param inTrackInfo track info object
         */
        public MenuManager(JFrame inParent, App inApp, TrackInfo inTrackInfo)
        {
@@ -136,7 +139,7 @@ public class MenuManager implements DataSubscriber
                _saveItem.addActionListener(_saveAction);
                _saveItem.setEnabled(false);
                fileMenu.add(_saveItem);
-               // Export
+               // Export - Kml
                _exportKmlItem = new JMenuItem(I18nManager.getText("menu.file.exportkml"));
                _exportKmlItem.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
@@ -146,6 +149,17 @@ public class MenuManager implements DataSubscriber
                });
                _exportKmlItem.setEnabled(false);
                fileMenu.add(_exportKmlItem);
+               // Gpx
+               _exportGpxItem = new JMenuItem(I18nManager.getText("menu.file.exportgpx"));
+               _exportGpxItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.exportGpx();
+                       }
+               });
+               _exportGpxItem.setEnabled(false);
+               fileMenu.add(_exportGpxItem);
+               // Pov
                _exportPovItem = new JMenuItem(I18nManager.getText("menu.file.exportpov"));
                _exportPovItem.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
@@ -360,7 +374,18 @@ public class MenuManager implements DataSubscriber
                };
                _connectPhotoItem.addActionListener(_connectPhotoAction);
                _connectPhotoItem.setEnabled(false);
+               photoMenu.addSeparator();
                photoMenu.add(_connectPhotoItem);
+               // disconnect photo
+               _disconnectPhotoItem = new JMenuItem(I18nManager.getText("menu.photo.disconnect"));
+               _disconnectPhotoItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.disconnectPhotoFromPoint();
+                       }
+               });
+               _disconnectPhotoItem.setEnabled(false);
+               photoMenu.add(_disconnectPhotoItem);
                _deletePhotoItem = new JMenuItem(I18nManager.getText("menu.photo.delete"));
                _deletePhotoItem.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
@@ -370,6 +395,17 @@ public class MenuManager implements DataSubscriber
                });
                _deletePhotoItem.setEnabled(false);
                photoMenu.add(_deletePhotoItem);
+               photoMenu.addSeparator();
+               // correlate all photos
+               _correlatePhotosItem = new JMenuItem(I18nManager.getText("menu.photo.correlate"));
+               _correlatePhotosItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.beginCorrelatePhotos();
+                       }
+               });
+               _correlatePhotosItem.setEnabled(false);
+               photoMenu.add(_correlatePhotosItem);
                menubar.add(photoMenu);
 
                // Add 3d menu (whether java3d available or not)
@@ -385,8 +421,16 @@ public class MenuManager implements DataSubscriber
                threeDMenu.add(_show3dItem);
                menubar.add(threeDMenu);
 
-               // Help menu for About
+               // Help menu
                JMenu helpMenu = new JMenu(I18nManager.getText("menu.help"));
+               JMenuItem helpItem = new JMenuItem(I18nManager.getText("menu.help"));
+               helpItem.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _app.showHelp();
+                       }
+               });
+               helpMenu.add(helpItem);
                JMenuItem aboutItem = new JMenuItem(I18nManager.getText("menu.help.about"));
                aboutItem.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
@@ -481,6 +525,7 @@ public class MenuManager implements DataSubscriber
                _saveItem.setEnabled(hasData);
                _saveButton.setEnabled(hasData);
                _exportKmlItem.setEnabled(hasData);
+               _exportGpxItem.setEnabled(hasData);
                _exportPovItem.setEnabled(hasData);
                _deleteDuplicatesItem.setEnabled(hasData);
                _compressItem.setEnabled(hasData);
@@ -505,15 +550,18 @@ public class MenuManager implements DataSubscriber
                _selectEndItem.setEnabled(hasPoint);
                _selectEndButton.setEnabled(hasPoint);
                // are there any photos?
-               _saveExifItem.setEnabled(_photos != null && _photos.getNumPhotos() > 0);
+               boolean anyPhotos = _photos != null && _photos.getNumPhotos() > 0;
+               _saveExifItem.setEnabled(anyPhotos);
                // is there a current photo?
-               boolean hasPhoto = _photos != null && _photos.getNumPhotos() > 0
-                       && _selection.getCurrentPhotoIndex() >= 0;
+               boolean hasPhoto = anyPhotos && _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);
+               _disconnectPhotoItem.setEnabled(hasPhoto && _photos.getPhoto(_selection.getCurrentPhotoIndex()) != null
+                       && _photos.getPhoto(_selection.getCurrentPhotoIndex()).getDataPoint() != null);
+               _correlatePhotosItem.setEnabled(anyPhotos && hasData);
                _deletePhotoItem.setEnabled(hasPhoto);
                // is there a current range?
                boolean hasRange = (hasData && _selection.hasRangeSelected());
index a8c94eb76058d4de191ee338527e2a099506978f..b792a66f55b6b70ff820a8d539739fa51c096787 100644 (file)
@@ -5,7 +5,6 @@ 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;
 
@@ -92,29 +91,39 @@ public class PhotoThumbnail extends JPanel implements Runnable
         */
        public void run()
        {
-               int picWidth = _photo.getWidth();
-               int picHeight = _photo.getHeight();
-               if (picWidth > -1 && picHeight > -1)
+               // Use exif thumbnail?
+               if (_photo.getExifThumbnail() != null) {
+                       Image image = new ImageIcon(_photo.getExifThumbnail()).getImage();
+                       _thumbnail = ImageUtils.createScaledImage(image, image.getWidth(null), image.getHeight(null));
+                       image = null;
+               }
+               else
                {
-                       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)
+                       // no exif thumbnail available, going to have to read whole thing
+                       int picWidth = _photo.getWidth();
+                       int picHeight = _photo.getHeight();
+                       if (picWidth > -1 && picHeight > -1)
                        {
-                               // 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));
+                               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;
index 8bc2c055a5301910b8c3a71cc1b3708204794078..22724b5f6a431d674396f626393e6c8d2ca1d464 100644 (file)
@@ -178,6 +178,7 @@ public class ProfileChart extends GenericChart
 
        /**
         * Method to inform map that data has changed
+        * @param inTrack track object
         */
        public void dataUpdated(Track inTrack)
        {
index 38e41652fca02918fcbeb3209ac34c748ba95ace..203db4527412f79a5110c8d46905cddc1afc52aa 100644 (file)
@@ -4,6 +4,7 @@ import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Font;
+import java.awt.GridLayout;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
 
@@ -92,7 +93,7 @@ public class SelectorDisplay extends GenericDisplay
 
                // Add panel for waypoints / photos
                JPanel listsPanel = new JPanel();
-               listsPanel.setLayout(new BoxLayout(listsPanel, BoxLayout.Y_AXIS));
+               listsPanel.setLayout(new GridLayout(0, 1));
                listsPanel.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
                );
@@ -104,8 +105,12 @@ public class SelectorDisplay extends GenericDisplay
                        {
                                if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex());
                        }});
-               listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints")));
-               listsPanel.add(new JScrollPane(_waypointList));
+               JPanel waypointListPanel = new JPanel();
+               waypointListPanel.setLayout(new BorderLayout());
+               waypointListPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints")), BorderLayout.NORTH);
+               waypointListPanel.add(new JScrollPane(_waypointList), BorderLayout.CENTER);
+               listsPanel.add(waypointListPanel);
+               // photo list
                _photoListModel = new PhotoListModel(_trackInfo.getPhotoList());
                _photoList = new JList(_photoListModel);
                _photoList.setVisibleRowCount(NUM_LIST_ENTRIES);
@@ -114,8 +119,11 @@ public class SelectorDisplay extends GenericDisplay
                        {
                                if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex());
                        }});
-               listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos")));
-               listsPanel.add(new JScrollPane(_photoList));
+               JPanel photoListPanel = new JPanel();
+               photoListPanel.setLayout(new BorderLayout());
+               photoListPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos")), BorderLayout.NORTH);
+               photoListPanel.add(new JScrollPane(_photoList), BorderLayout.CENTER);
+               listsPanel.add(photoListPanel);
                listsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
 
                // add the controls to the main panel
@@ -123,10 +131,11 @@ public class SelectorDisplay extends GenericDisplay
                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);
+               // and lists in the centre
+               add(listsPanel, BorderLayout.CENTER);
                // set preferred width to be small
                setPreferredSize(new Dimension(100, 100));
        }
index 5c3a992b431f0f7e9edcb65b8d6aa6b6728cc4b5..46a1c8d9de834f231b7a48f4607d33144665d2c5 100644 (file)
@@ -33,6 +33,8 @@ public class UndoManager
 
        /**
         * Constructor
+        * @param inApp App object
+        * @param inFrame parent frame
         */
        public UndoManager(App inApp, JFrame inFrame)
        {
@@ -67,7 +69,7 @@ public class UndoManager
                // Buttons
                JPanel buttonPanel = new JPanel();
                buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
-               JButton okButton = new JButton("OK");
+               JButton okButton = new JButton(I18nManager.getText("button.ok"));
                okButton.addActionListener(new ActionListener()
                        {
                                public void actionPerformed(ActionEvent e)
@@ -77,7 +79,7 @@ public class UndoManager
                                }
                        });
                buttonPanel.add(okButton);
-               JButton cancelButton = new JButton("Cancel");
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
                cancelButton.addActionListener(new ActionListener()
                        {
                                public void actionPerformed(ActionEvent e)
index 5214018858bb264fa3e03487c42b90678ed8beb7..b9d86eb21d8669f3979fa57830c48259baa61b29 100644 (file)
@@ -4,9 +4,10 @@
 # Menu entries
 menu.file=File
 menu.file.open=Open
-menu.file.addphotos=Add Photos
+menu.file.addphotos=Add photos
 menu.file.save=Save
 menu.file.exportkml=Export KML
+menu.file.exportgpx=Export GPX
 menu.file.exportpov=Export POV
 menu.file.exit=Exit
 menu.edit=Edit
@@ -32,6 +33,8 @@ menu.select.end=Set range end
 menu.photo=Photo
 menu.photo.saveexif=Save to Exif
 menu.photo.connect=Connect to point
+menu.photo.disconnect=Disconnect from point
+menu.photo.correlate=Correlate all photos
 menu.photo.delete=Remove photo
 menu.3d=Three-D
 menu.3d.show3d=Show in Three-D
@@ -41,6 +44,7 @@ menu.help.about=About Prune
 menu.map.zoomin=Zoom in
 menu.map.zoomout=Zoom out
 menu.map.zoomfull=Zoom to full scale
+menu.map.connect=Connect track points
 menu.map.autopan=Autopan
 
 # Dialogs
@@ -92,8 +96,8 @@ dialog.save.table.hasdata=Has data
 dialog.save.table.save=Save
 dialog.save.headerrow=Output header row
 dialog.save.coordinateunits=Coordinate units
-dialog.save.units.original=Original
 dialog.save.altitudeunits=Altitude units
+dialog.save.timestampformat=Timestamp format
 dialog.save.oktitle=File saved
 dialog.save.ok1=Successfully saved
 dialog.save.ok2=points to file
@@ -101,9 +105,14 @@ 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=Title for the data
+dialog.exportkml.altitude=Include altitudes (for aviation)
 dialog.exportkml.kmz=Compress to make kmz file
 dialog.exportkml.exportimages=Export image thumbnails to kmz
 dialog.exportkml.filetype=KML, KMZ files
+dialog.exportgpx.title=Export GPX
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Description
+dialog.exportgpx.filetype=GPX files
 dialog.exportpov.title=Export POV
 dialog.exportpov.text=Please enter the parameters for the POV export
 dialog.exportpov.font=Font
@@ -150,6 +159,32 @@ dialog.saveexif.photostatus.modified=Modified
 dialog.saveexif.overwrite=Overwrite files
 dialog.saveexif.ok1=Saved
 dialog.saveexif.ok2=photo files
+dialog.correlate.title=Correlate photos
+dialog.correlate.notimestamps=There are no timestamps in the data points, so there is nothing to correlate with the photos.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated photos.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated photos to use as the time offset
+dialog.correlate.photoselect.photoname=Photo name
+dialog.correlate.photoselect.timediff=Time difference
+dialog.correlate.photoselect.photolater=Photo later
+dialog.correlate.options.tip=Tip: By manually correlating at least one photo, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Time offset
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=hours,
+dialog.correlate.options.offset.minutes=minutes and
+dialog.correlate.options.offset.seconds=seconds
+dialog.correlate.options.photolater=Photo later than point
+dialog.correlate.options.pointlater=Point later than photo
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All photos are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one photo.
+dialog.correlate.confirmsingle.text=photo was correlated
+dialog.correlate.confirmmultiple.text=photos were correlated
+dialog.help.help=Please see\n http://activityworkshop.net/software/prune/\nfor more information and user guides.
 dialog.about.title=About Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -169,6 +204,7 @@ 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.translators=Translators
 dialog.about.credits.translations=Translations helped by
 dialog.about.credits.devtools=Development tools
 dialog.about.credits.othertools=Other tools
@@ -190,8 +226,6 @@ button.cancel=Cancel
 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=Edit
 button.exit=Exit
@@ -203,6 +237,8 @@ button.yestoall=Yes to all
 button.notoall=No to all
 button.selectall=Select all
 button.selectnone=Select none
+button.preview=Preview
+button.guessfields=Guess fields
 
 # Display components
 display.nodata=No data loaded
@@ -224,6 +260,7 @@ details.range.to=to
 details.altitude.to=to
 details.range.climb=Climb
 details.range.descent=Descent
+details.coordformat=Coordinate format
 details.distanceunits=Distance units
 display.range.time.secs=s
 display.range.time.mins=m
@@ -234,6 +271,7 @@ details.waypointsphotos.photos=Photos
 details.photodetails=Photo details
 details.nophoto=No photo selected
 details.photo.loading=Loading
+details.photo.connected=Connected
 
 # Field names
 fieldname.latitude=Latitude
@@ -249,6 +287,8 @@ fieldname.distance=Distance
 fieldname.duration=Duration
 
 # Measurement units
+units.original=Original
+units.default=Default
 units.metres=Metres
 units.metres.short=m
 units.feet=Feet
@@ -260,6 +300,7 @@ units.miles.short=mi
 units.degminsec=Deg-min-sec
 units.degmin=Deg-min
 units.deg=Degrees
+units.iso8601=ISO 8601
 
 # Cardinals for 3d plots
 cardinal.n=N
@@ -280,6 +321,8 @@ undo.deleteduplicates=delete duplicates
 undo.reverse=reverse range
 undo.rearrangewaypoints=rearrange waypoints
 undo.connectphoto=connect photo
+undo.disconnectphoto=disconnect photo
+undo.correlate=correlate photos
 
 # Error messages
 error.save.dialogtitle=Error saving data
index 3a53f655ef355e47f629ae9b556342b17575eb2b..7c67508e83330e41483a27f48f1e42d6dbb576a2 100644 (file)
@@ -7,6 +7,7 @@ menu.file.open=
 menu.file.addphotos=Fotos laden
 menu.file.save=Speichern
 menu.file.exportkml=KML exportieren
+menu.file.exportgpx=GPX exportieren
 menu.file.exportpov=POV exportieren
 menu.file.exit=Beenden
 menu.edit=Bearbeiten
@@ -32,6 +33,8 @@ menu.select.end=Stopp setzen
 menu.photo=Foto
 menu.photo.saveexif=Exif Daten speichern
 menu.photo.connect=Mit Punkt verbinden
+menu.photo.disconnect=Vom Punkt trennen
+menu.photo.correlate=Alle Fotos korrelieren
 menu.photo.delete=Foto entfernen
 menu.3d=Drei-D
 menu.3d.show3d=In drei-D zeigen
@@ -41,6 +44,7 @@ menu.help.about=
 menu.map.zoomin=Einzoomen
 menu.map.zoomout=Auszoomen
 menu.map.zoomfull=Zoomen zum ganzes Bild
+menu.map.connect=Trackpunkte mit Linie
 menu.map.autopan=Autopan
 
 # Dialogs
@@ -92,8 +96,8 @@ dialog.save.table.hasdata=Hat Daten
 dialog.save.table.save=Speichern
 dialog.save.headerrow=Titel Zeile speichern
 dialog.save.coordinateunits=Koordinaten Maßeinheiten
-dialog.save.units.original=Original
 dialog.save.altitudeunits=Höhe Maßeinheiten
+dialog.save.timestampformat=Zeitstempelformat
 dialog.save.oktitle=Datei gespeichert
 dialog.save.ok1=Es wurden
 dialog.save.ok2=Punkte gespeichert nach
@@ -101,9 +105,14 @@ 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=Titel für die Daten
+dialog.exportkml.altitude=Auch Höheninformation (für Luftfahrt)
 dialog.exportkml.kmz=Daten ins kmz Datei komprimieren
 dialog.exportkml.exportimages=Bilder ins kmz exportieren
 dialog.exportkml.filetype=KML, KMZ Dateien
+dialog.exportgpx.title=GPX exportieren
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Beschreibung
+dialog.exportgpx.filetype=GPX Dateien
 dialog.exportpov.title=POV exportieren
 dialog.exportpov.text=Geben Sie die Parameter ein für das POV Export
 dialog.exportpov.font=Font
@@ -140,7 +149,7 @@ 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.noexiftool=Kein exiftool Programm gefunden. Trotzdem fortfahren?
 dialog.saveexif.table.photoname=Foto Name
 dialog.saveexif.table.status=Status
 dialog.saveexif.table.save=Speichern
@@ -150,6 +159,32 @@ dialog.saveexif.photostatus.modified=Modifiziert
 dialog.saveexif.overwrite=Dateien Ã¼berschreiben
 dialog.saveexif.ok1=Es wurden
 dialog.saveexif.ok2=Foto Dateien geschrieben
+dialog.correlate.title=Fotos korrelieren
+dialog.correlate.notimestamps=Die Punkte haben keine Zeitinformation, deswegen ist es nicht möglich die Fotos zu korrelieren.
+dialog.correlate.nouncorrelatedphotos=Alle Photos sind schon korreliert.\nWollen Sie trotzdem fortsetzen?
+dialog.correlate.photoselect.intro=Selektieren Sie einen von diesen Fotos um die Differenz zu berechnen
+dialog.correlate.photoselect.photoname=Foto Name
+dialog.correlate.photoselect.timediff=Zeitdifferenz
+dialog.correlate.photoselect.photolater=Foto später
+dialog.correlate.options.tip=Tipp: Mit mindestens einem korrelierten Foto, die Zeitdifferenz kann automatisch berechnet werden.
+dialog.correlate.options.intro=Wählen Sie die Optionen aus für die Korrelation
+dialog.correlate.options.offsetpanel=Zeitunterschied
+dialog.correlate.options.offset=Unterschied
+dialog.correlate.options.offset.hours=Stunden,
+dialog.correlate.options.offset.minutes=Minuten und
+dialog.correlate.options.offset.seconds=Sekunden
+dialog.correlate.options.photolater=Foto später als Punkt
+dialog.correlate.options.pointlater=Punkt später als Foto
+dialog.correlate.options.limitspanel=Korrelation Grenzen
+dialog.correlate.options.notimelimit=Keine Zeitgrenzen
+dialog.correlate.options.timelimit=Zeitgrenzen
+dialog.correlate.options.nodistancelimit=Keine Distanzgrenzen
+dialog.correlate.options.distancelimit=Distanzgrenzen
+dialog.correlate.options.correlate=Korrelieren
+dialog.correlate.alloutsiderange=Alle Fotos sind ausserhalb vom Track Zeitraum, so können nicht korreliert werden.\nVersuchen Sie mit einem anderen Offset oder verbinden Sie manuell mindestens ein Foto.
+dialog.correlate.confirmsingle.text=Foto wurde korreliert
+dialog.correlate.confirmmultiple.text=Fotos wurden korreliert
+dialog.help.help=Bitte sehen Sie\n http://activityworkshop.net/software/prune/\nfür weitere Information und Benutzeranleitungen.
 dialog.about.title=Ãœber Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -169,6 +204,7 @@ 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.translators=Dolmetscher
 dialog.about.credits.translations=Ãœbersetzungen mit Hilfe von
 dialog.about.credits.devtools=Entwicklungsprogrammen
 dialog.about.credits.othertools=Andere Programmen
@@ -190,8 +226,6 @@ button.cancel=Abbrechen
 button.overwrite=Ãœberschreiben
 button.moveup=Aufwärts moven
 button.movedown=Abwärts moven
-button.deletepoint=Punkt löschen
-button.deleterange=Spanne löschen
 button.showlines=Linien anzeigen
 button.edit=Bearbeiten
 button.exit=Beenden
@@ -203,6 +237,8 @@ button.yestoall=Ja f
 button.notoall=Nein für alle
 button.selectall=Alle selektieren
 button.selectnone=Nichts selektieren
+button.preview=Vorschauen
+button.guessfields=Felder erraten
 
 # Display components
 display.nodata=Keine Daten geladen
@@ -224,6 +260,7 @@ details.range.to=bis
 details.altitude.to=bis
 details.range.climb=Aufstieg
 details.range.descent=Abstieg
+details.coordformat=Koordinatenformat
 details.distanceunits=Distanz Maßeinheiten
 display.range.time.secs=S
 display.range.time.mins=M
@@ -234,6 +271,7 @@ details.waypointsphotos.photos=Fotos
 details.photodetails=Details vom Foto
 details.nophoto=Kein Foto selektiert
 details.photo.loading=Laden
+details.photo.connected=Verbunden
 
 # Field names
 fieldname.latitude=Breitengrad
@@ -249,6 +287,8 @@ fieldname.distance=L
 fieldname.duration=Zeitlänge
 
 # Measurement units
+units.original=Original
+units.default=Default
 units.metres=Meter
 units.metres.short=M
 units.feet=Füße
@@ -260,6 +300,7 @@ units.miles.short=Mei
 units.degminsec=Grad-Min-Sek
 units.degmin=Grad-Min
 units.deg=Grad
+units.iso8601=ISO 8601
 
 # Cardinals for 3d plots
 cardinal.n=N
@@ -280,6 +321,8 @@ undo.deleteduplicates=Duplikaten l
 undo.reverse=Spanne umdrehen
 undo.rearrangewaypoints=Waypoints reorganisieren
 undo.connectphoto=Foto verbinden
+undo.disconnectphoto=Foto trennen
+undo.correlate=Fotos korrelieren
 
 # Error messages
 error.save.dialogtitle=Fehler beim Speichern
index 7b2013eab37282eb2176f37f252eb7e4827d5501..9ce937ae11e877167a910ac2aea0c2c85f00cc8e 100644 (file)
@@ -7,6 +7,7 @@ menu.file.open=
 menu.file.addphotos=Fötelis innätue
 menu.file.save=Speichere
 menu.file.exportkml=KML exportiere
+menu.file.exportgpx=GPX exportiere
 menu.file.exportpov=POV exportiere
 menu.file.exit=Beände
 menu.edit=Editiere
@@ -32,6 +33,8 @@ menu.select.end=Stopp setz
 menu.photo=Föteli
 menu.photo.saveexif=Exif Date speicherä
 menu.photo.connect=Mitem Punkt verbindä
+menu.photo.disconnect=Vonem Punkt trännä
+menu.photo.correlate=Alli Fötelis korrelierä
 menu.photo.delete=Föteli entfernä
 menu.3d=Drüü-D
 menu.3d.show3d=In drüü-D zeigä
@@ -41,6 +44,7 @@ menu.help.about=
 menu.map.zoomin=Einzoome
 menu.map.zoomout=Uuszoome
 menu.map.zoomfull=Zoome zum ganzes Bild
+menu.map.connect=Trackpünktli verbindä
 menu.map.autopan=Autopan
 
 # Dialogs
@@ -92,26 +96,31 @@ dialog.save.table.hasdata=Het Date
 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.timestampformat=Ziitstämpelformat
+dialog.save.oktitle=File gspeicheret worde
 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 exportierä
 dialog.exportkml.text=Titel für die Date
+dialog.exportkml.altitude=Au Höchiinformation (fürs Fliege)
 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.exportgpx.title=GPX exportierä
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Beschriibig
+dialog.exportgpx.filetype=GPX Dateie
+dialog.exportpov.title=POV exportierä
 dialog.exportpov.text=Gäbet Sie die Parameter ii fürs POV Export
 dialog.exportpov.font=Font
 dialog.exportpov.camerax=Kamera X
 dialog.exportpov.cameray=Kamera Y
 dialog.exportpov.cameraz=Kamera Z
 dialog.exportpov.filetype=POV Dateie
-dialog.exportpov.warningtracksize=Dieser Track hät sehr viele Punkte, die Java3D villiicht nöd chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze?
+dialog.exportpov.warningtracksize=Dieser Track hät mega viele Punkte, die Java3D villiicht nöd chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze?
 dialog.confirmreversetrack.title=Umdrehig bestätige
 dialog.confirmreversetrack.text=Diese Daten enthalte Ziitstämpel Informatione, die bei dr Umkehrig usser Reihefolge erschiene würdi.\nSind Sie sicher, Sie wend diese Spanne umkehre?
 dialog.interpolate.title=Punkte interpoliere
@@ -123,9 +132,9 @@ dialog.confirmundo.single.text=Operation r
 dialog.confirmundo.multiple.text=Operatione rückgängig gmacht worde.
 dialog.undo.none.title=Undo nöd möglich
 dialog.undo.none.text=Keini Operatione könne rückgängig gmacht werde.
-dialog.clearundo.title=Undo-Liste lösche
+dialog.clearundo.title=Undo-Liste löschä
 dialog.clearundo.text=Sind Sie sicher, Sie wend die Undo-Liste lösche?\nAlle Undo Infos werdet verlore gah!
-dialog.pointedit.title=Punkt editiere
+dialog.pointedit.title=Punkt editierä
 dialog.pointedit.text=Wählet Sie jäden Fäld uus zu editiere, und mitem 'Editiere' Chnopf den Wert Ã¤ndere
 dialog.pointedit.table.field=Fäld
 dialog.pointedit.table.value=Wert
@@ -150,6 +159,32 @@ dialog.saveexif.photostatus.modified=G
 dialog.saveexif.overwrite=Files Ã¼berschriebä
 dialog.saveexif.ok1=Es sin
 dialog.saveexif.ok2=Fötelis gschriebe worde
+dialog.correlate.title=Fötelis korrelierä
+dialog.correlate.notimestamps=Es hät kei Ziitstämpel inem Track innä, so s'isch nöd möglech die Fötelis zu korrelierä.
+dialog.correlate.nouncorrelatedphotos=Alle Fötelis sin scho korreliert.\nWend Sie trotzdem fortsetzä?
+dialog.correlate.photoselect.intro=Wählet Sie eini vo deren Föteli uus um die Ziitdifferänz zu berächnä
+dialog.correlate.photoselect.photoname=Föteli Name
+dialog.correlate.photoselect.timediff=Ziitdifferänz
+dialog.correlate.photoselect.photolater=Föteli spöter
+dialog.correlate.options.tip=Tipp: Mit mindeschtens einem korrelierten Föteli, die Ziitdifferänz kann automatisch berächnet werdä.
+dialog.correlate.options.intro=Wählet Sie die Optione uus für die Korrelierig
+dialog.correlate.options.offsetpanel=Ziitunterschied
+dialog.correlate.options.offset=Unterschied
+dialog.correlate.options.offset.hours=Schtundä,
+dialog.correlate.options.offset.minutes=Minutä und
+dialog.correlate.options.offset.seconds=Sekundä
+dialog.correlate.options.photolater=Föteli spöter alsem Punkt
+dialog.correlate.options.pointlater=Punkt spöter alsem Föteli
+dialog.correlate.options.limitspanel=Korrelation Gränzä
+dialog.correlate.options.notimelimit=Kei Ziitgränzä
+dialog.correlate.options.timelimit=Ziitgränzä
+dialog.correlate.options.nodistancelimit=Kei Distanzgränzä
+dialog.correlate.options.distancelimit=Distanzgränzä
+dialog.correlate.options.correlate=Korrelierä
+dialog.correlate.alloutsiderange=Alli Fötelis sin uusserhalb vonem Track Ziitruum, so chönne nöd korreliert werdä.\nVersuechet Sie mitenem anderen Offset oder verbindet Sie manuell mindeschtens eis Föteli.
+dialog.correlate.confirmsingle.text=Föteli isch korreliert worde
+dialog.correlate.confirmmultiple.text=Fötelis sin korreliert worde
+dialog.help.help=Bitte lueg na\n http://activityworkshop.net/software/prune/\nfür wiitere Information und Benutzeraaleitige.
 dialog.about.title=Ãœber Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -169,6 +204,7 @@ 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.translators=Dolmätscher
 dialog.about.credits.translations=Ãœbersetzige mit dr Hilfe vo
 dialog.about.credits.devtools=Entwicklungswärkzüüge
 dialog.about.credits.othertools=Anderi Wärkzüüge
@@ -190,8 +226,6 @@ button.cancel=Abbr
 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ä
@@ -203,6 +237,8 @@ button.yestoall=Ja f
 button.notoall=Nei für alli
 button.selectall=Alli selektierä
 button.selectnone=Nüüt selektierä
+button.preview=Vorschauä
+button.guessfields=Fälde erratä
 
 # Display components
 display.nodata=Kei Date glade worde
@@ -224,7 +260,8 @@ details.range.to=bis
 details.altitude.to=bis
 details.range.climb=Uufstieg
 details.range.descent=Abstieg
-details.distanceunits=Distanz Masseinheiten
+details.coordformat=Koordinatenformat
+details.distanceunits=Distanz Masseinheite
 display.range.time.secs=S
 display.range.time.mins=M
 display.range.time.hours=Std
@@ -234,6 +271,7 @@ details.waypointsphotos.photos=F
 details.photodetails=Details vom Föteli
 details.nophoto=Kei föteli selektiert
 details.photo.loading=Ladä
+details.photo.connected=Verbundä
 
 # Field names
 fieldname.latitude=Breitegrad
@@ -249,6 +287,8 @@ fieldname.distance=L
 fieldname.duration=Ziitlängi
 
 # Measurement units
+units.original=Original
+units.default=Default
 units.metres=Meter
 units.metres.short=M
 units.feet=Fuess
@@ -260,6 +300,7 @@ units.miles.short=Mei
 units.degminsec=Grad-Min-Sek
 units.degmin=Grad-Min
 units.deg=Grad
+units.iso8601=ISO 8601
 
 # Cardinals for 3d plots
 cardinal.n=N
@@ -280,20 +321,22 @@ undo.deleteduplicates=Duplikaten l
 undo.reverse=Spanne umdrähie
 undo.rearrangewaypoints=Waypoints reorganisierä
 undo.connectphoto=Föteli verbindä
+undo.disconnectphoto=Föteli trännä
+undo.correlate=Fötelis korrelierä
 
 # Error messages
-error.save.dialogtitle=Fehler bim Speichere
+error.save.dialogtitle=Fähle 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.dialogtitle=Fähle bim Lade
 error.load.noread=File cha nöd glase werde
-error.load.nopoints=Kei gültigi Information im Datei gfunde
+error.load.nopoints=Kei gültigi Information inem File gfunde
 error.load.unknownxml=Unbekanntes xml Format:
-error.load.othererror=Fehler bim Läse:
-error.jpegload.dialogtitle=Fehler bim Lade von Fötelis
+error.load.othererror=Fähle bim Läse:
+error.jpegload.dialogtitle=Fähle bim Lade von Fötelis
 error.jpegload.nofilesfound=Kei Dateie gfunde
 error.jpegload.nojpegsfound=Kei Jpegs gfunde
 error.jpegload.noexiffound=Kei EXIF Information gfunde
@@ -301,9 +344,9 @@ error.jpegload.nogpsfound=Kei GPS Information gfunde
 error.undofailed.title=Undo isch fehlgschlage worde
 error.undofailed.text=Operation kann nöd rückgängig gmacht werde
 error.function.noop.title=Funktion hät gar nüüt gmacht
-error.rearrange.noop=Waypoints Reorganisiere hät kein Effäkt gha
+error.rearrange.noop=Waypoints Reorganisierig hät kei Effäkt gha
 error.function.notimplemented=Sorry, d'Funktion isch nonig implementiert worde.
 error.function.notavailable.title=Funktion nöd verfüegbar
 error.function.nojava3d=Sorry, d'Funktion brucht d Java3d Library,\nvo Sun.com odr Blackdown.org erhältlech.
-error.3d.title=Fähler mitm 3d Darstellig
-error.3d=N Fähler isch mitm 3d Darstellig ufgtrete
+error.3d.title=Fähler mitere 3d Darstellig
+error.3d=N Fähler isch mitere 3d Darstellig ufgtrete
index fcd83cf5a7c58295b0a37188556f20e90ad13baa..f6746c713f5eabd48d1b2f332461eb1ecb5ae452 100644 (file)
@@ -7,6 +7,7 @@ menu.file.open=Abrir
 menu.file.addphotos=Cargar fotos
 menu.file.save=Guardar
 menu.file.exportkml=Exportar KML
+menu.file.exportgpx=Exportar GPX
 menu.file.exportpov=Exportar POV
 menu.file.exit=Salir
 menu.edit=Editar
@@ -26,12 +27,14 @@ menu.edit.rearrange.end=Ir al final
 menu.edit.rearrange.nearest=Ir al más próximo
 menu.select=Seleccionar
 menu.select.all=Seleccionar todo
-menu.select.none=Seleccionar nada
+menu.select.none=No 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.connect=Conectar con punto
+menu.photo.disconnect=Desconectar de punto
+menu.photo.correlate=Correlacionar todas las fotos
 menu.photo.delete=Eliminar foto
 menu.3d=3-D
 menu.3d.show3d=Mostrar en 3-D
@@ -41,7 +44,8 @@ menu.help.about=Acerca de Prune
 menu.map.zoomin=Ampliar zoom
 menu.map.zoomout=Reducir zoom
 menu.map.zoomfull=Mostrar todo
-menu.map.autopan=Posicionar automático
+menu.map.connect=Conectar puntos de track
+menu.map.autopan=Posicionar automáticamente
 
 # Dialogs
 dialog.exit.confirm.title=Salir de Prune
@@ -49,9 +53,9 @@ 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.deletepoint.deletephoto=Borrar la foto tambien?
 dialog.deletephoto.title=Borrar foto
-dialog.deletephoto.deletepoint=Borrar punto tambien?
+dialog.deletephoto.deletepoint=Borrar el punto tambien?
 dialog.deleteduplicates.title=Borrar duplicados
 dialog.deleteduplicates.single.text=duplicado eliminado
 dialog.deleteduplicates.multi.text=duplicados eliminados
@@ -73,7 +77,7 @@ dialog.delimiter.tab=Tabulador
 dialog.delimiter.space=Espacio
 dialog.delimiter.semicolon=Punto y coma ;
 dialog.delimiter.other=Otro
-dialog.openoptions.deliminfo.records=datos, con 
+dialog.openoptions.deliminfo.records=datos, con
 dialog.openoptions.deliminfo.fields=campos
 dialog.openoptions.deliminfo.norecords=Ningun dato
 dialog.openoptions.tabledesc=Extraer archivo
@@ -92,24 +96,29 @@ dialog.save.table.hasdata=Contiene datos
 dialog.save.table.save=Guardar
 dialog.save.headerrow=Título fila
 dialog.save.coordinateunits=Unidades de las coordenadas
-dialog.save.units.original=Original
 dialog.save.altitudeunits=Unidades de las altitudes
-dialog.save.oktitle=Guardando archivo 
+dialog.save.timestampformat=Format del tiempo
+dialog.save.oktitle=Guardando archivo
 dialog.save.ok1=Guardando
 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=Descripción para los datos
+dialog.exportkml.altitude=Incluir altitudes (para aviación)
 dialog.exportkml.kmz=Comprimir al archivo kmz
 dialog.exportkml.exportimages=Exportar fotos al kmz
 dialog.exportkml.filetype=Archivos KML, KMZ
+dialog.exportgpx.title=Exportar GPX
+dialog.exportgpx.name=Nombre
+dialog.exportgpx.desc=Descripción
+dialog.exportgpx.filetype=Archivos GPX
 dialog.exportpov.title=Exportar POV
 dialog.exportpov.text=Introdzca los Parametros para exportar
 dialog.exportpov.font=Fuente
-dialog.exportpov.camerax=Camera X
-dialog.exportpov.cameray=Camera Y
-dialog.exportpov.cameraz=Camera Z
+dialog.exportpov.camerax=Cámara X
+dialog.exportpov.cameray=Cámara Y
+dialog.exportpov.cameraz=Cámara Z
 dialog.exportpov.filetype=Archivos POV
 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
@@ -134,22 +143,48 @@ dialog.pointedit.changevalue.text=Introduzca el nuevo valor de campo
 dialog.pointedit.changevalue.title=Editar campo
 dialog.pointnameedit.title=Editar nombre de waypoint
 dialog.pointnameedit.name=Nombre de waypoint
-dialog.pointnameedit.uppercase=Maysculas
-dialog.pointnameedit.lowercase=minsculas
+dialog.pointnameedit.uppercase=Mayúsculas
+dialog.pointnameedit.lowercase=minúsculas
 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.nothingtosave=Coordenadas no modificadas, nada que guardar
 dialog.saveexif.noexiftool=exiftool program no encontrado. Desea continuar?
-dialog.saveexif.table.photoname=Nombre de foto
-dialog.saveexif.table.status=Status
+dialog.saveexif.table.photoname=Nombre de la foto
+dialog.saveexif.table.status=Estado
 dialog.saveexif.table.save=Guardar
-dialog.saveexif.photostatus.connected=Connected
-dialog.saveexif.photostatus.disconnected=Disconnected
-dialog.saveexif.photostatus.modified=Modificado
+dialog.saveexif.photostatus.connected=Conectada
+dialog.saveexif.photostatus.disconnected=Desconectada
+dialog.saveexif.photostatus.modified=Modificada
 dialog.saveexif.overwrite=Sobreescribirlar archivos?
 dialog.saveexif.ok1=Guardando
 dialog.saveexif.ok2=fotos
+dialog.correlate.title=Correlacionar fotos
+dialog.correlate.notimestamps=No hay información de tiempo para los puntos, así que no hay nada que correlacionar con las fotos.
+dialog.correlate.nouncorrelatedphotos=No hay fotos no correlacionadas.\nEstá seguro de que desea continuar?
+dialog.correlate.photoselect.intro=Seleccione una de estas fotos correlacionadas para usar como margen de tiempo
+dialog.correlate.photoselect.photoname=Nombre de la foto
+dialog.correlate.photoselect.timediff=Diferencia de tiempo
+dialog.correlate.photoselect.photolater=Foto más adelante
+dialog.correlate.options.tip=Sugerencia: Correlacionando al menos una foto manualmente, el margen de tiempo se calcula automáticamente.
+dialog.correlate.options.intro=Seleccionar las opciones para correlación automática
+dialog.correlate.options.offsetpanel=Margen de tiempo
+dialog.correlate.options.offset=Margen
+dialog.correlate.options.offset.hours=horas,
+dialog.correlate.options.offset.minutes=minutos y
+dialog.correlate.options.offset.seconds=segundos
+dialog.correlate.options.photolater=Foto después de punto
+dialog.correlate.options.pointlater=Punto después de foto
+dialog.correlate.options.limitspanel=Límites de correlación
+dialog.correlate.options.notimelimit=Sin límite de tiempo
+dialog.correlate.options.timelimit=Límite de tiempo
+dialog.correlate.options.nodistancelimit=Sin límite de distancia
+dialog.correlate.options.distancelimit=Límite de distancia
+dialog.correlate.options.correlate=Correlacionar
+dialog.correlate.alloutsiderange=Todas las fotos están fuera del margen horario del track, por lo que ninguna puede ser correlada.\nIntente cambiar el margen o correle manualmente al menos una foto.
+dialog.correlate.confirmsingle.text=foto fue correlada
+dialog.correlate.confirmmultiple.text=fotos fueron correladas
+dialog.help.help=Por favor, ver\n http://activityworkshop.net/software/prune/\npara más información y guías del usuario.
 dialog.about.title=Acerca de Prune
 dialog.about.version=Versión
 dialog.about.build=Construir
@@ -157,29 +192,30 @@ dialog.about.summarytext1=Prune es un programa para cargar, mostrar y editar dat
 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 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=Informacion del sistema
+dialog.about.systeminfo.os=Sistema operativo
 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.systeminfo.java3d=Java3d instalado
+dialog.about.systeminfo.povray=Povray instalado
+dialog.about.systeminfo.exiftool=Exiftool instalado
 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
+dialog.about.credits.code=El código de Prune fue escrito por
+dialog.about.credits.exifcode=El código Exif por
+dialog.about.credits.icons=Algunos iconos se tomaron de
+dialog.about.credits.translators=Traductores
+dialog.about.credits.translations=Ayuda en la traducción
+dialog.about.credits.devtools=Herramientas de desarrollo
+dialog.about.credits.othertools=Otras herramientas
+dialog.about.credits.thanks=Gracias a
 
 # 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
+dialog.3dlines.title=Cuadrícula Prune
+dialog.3dlines.empty=No hay ninguna cuadrícula!
+dialog.3dlines.intro=Información de la cuadrícula
 
 # Buttons
 button.ok=Aceptar
@@ -190,9 +226,7 @@ button.cancel=Cancelar
 button.overwrite=Sobreescribir
 button.moveup=Mover hacia arriba
 button.movedown=Mover hacia abajo
-button.deletepoint=Eliminar punto
-button.deleterange=Eliminar rango
-button.showlines=Mostrar gridlines
+button.showlines=Mostrar cuadrícula
 button.edit=Editar
 button.exit=Salir
 button.close=Cerrar
@@ -203,6 +237,8 @@ button.yestoall=Si por todo
 button.notoall=No por todo
 button.selectall=Seleccionar todo
 button.selectnone=Seleccionar nada
+button.preview=Previsualización
+button.guessfields=Adivinar campos
 
 # Display components
 display.nodata=Ningún dato cargado
@@ -224,6 +260,7 @@ details.range.to=hacia
 details.altitude.to=hacia
 details.range.climb=Ascenso
 details.range.descent=Descenso
+details.coordformat=Formato de coordenadas
 details.distanceunits=Unidades de distancia
 display.range.time.secs=s
 display.range.time.mins=m
@@ -232,8 +269,9 @@ 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
+details.nophoto=Ninguna foto seleccionada
+details.photo.loading=Cargando
+details.photo.connected=Conectada
 
 # Field names
 fieldname.latitude=Latitud
@@ -249,6 +287,8 @@ fieldname.distance=Distancia
 fieldname.duration=Duración
 
 # Measurement units
+units.original=Original
+units.default=Por defecto
 units.metres=Metros
 units.metres.short=m
 units.feet=Pies
@@ -260,6 +300,7 @@ units.miles.short=mi
 units.degminsec=Gra-min-seg
 units.degmin=Gra-min
 units.deg=Grados
+units.iso8601=ISO 8601
 
 # Cardinals for 3d plots
 cardinal.n=N
@@ -279,7 +320,9 @@ undo.insert=insertar puntos
 undo.deleteduplicates=eliminar duplicados
 undo.reverse=invertir rango
 undo.rearrangewaypoints=reordenar waypoints
-undo.connectphoto=connectar foto
+undo.connectphoto=conectar foto
+undo.disconnectphoto=desconectar foto
+undo.correlate=correlacionar fotos
 
 # Error messages
 error.save.dialogtitle=Fallo al guardar datos
@@ -291,7 +334,7 @@ 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.unknownxml=Formato xml no reconocido:
 error.load.othererror=Fallo al cargar datos:
 error.jpegload.dialogtitle=Error cargando fotos
 error.jpegload.nofilesfound=Ningún archivo encontrado
index 59e070bd13133ebc347cccecc500a1abfde821ca..adc2853940dcbc919de71be8e3e635a05aa6d505 100644 (file)
@@ -7,10 +7,11 @@ menu.file.open=Ouvrir
 menu.file.addphotos=Ouvrir photos
 menu.file.save=Enregistrer
 menu.file.exportkml=Exporter au KML
+menu.file.exportgpx=Exporter au GPX
 menu.file.exportpov=Exporter au POV
 menu.file.exit=Quitter
 menu.edit=Édition
-menu.edit.undo=Undo
+menu.edit.undo=Annuler
 menu.edit.clearundo=Purger undo liste
 menu.edit.editpoint=Editer point
 menu.edit.editwaypointname=Editer nom du waypoint
@@ -27,11 +28,13 @@ menu.edit.rearrange.nearest=Chaque 
 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.select.start=Set range début
+menu.select.end=Set range fin
 menu.photo=Photo
 menu.photo.saveexif=Enregistrer Ã  Exif
-menu.photo.connect=Connect to point
+menu.photo.connect=Relier au point
+menu.photo.disconnect=Disconnect from point
+menu.photo.correlate=Corréler tous les photos
 menu.photo.delete=Remove photo
 menu.3d=Trois-D
 menu.3d.show3d=Montrer en Trois-D
@@ -41,31 +44,32 @@ menu.help.about=
 menu.map.zoomin=Zoom avant
 menu.map.zoomout=Zoom arrière
 menu.map.zoomfull=Zoom to full scale
+menu.map.connect=Connect track points
 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.exit.confirm.text=Les données ont Ã©té modifié. Souhaitez-vous terminer Prune sans enregistrement?
+dialog.openappend.title=Append to existing données
+dialog.openappend.text=Append this to the données already loaded?
+dialog.deletepoint.title=Effacer point
+dialog.deletepoint.deletephoto=Effacer photo attached to this point?
+dialog.deletephoto.title=Effacer photo
+dialog.deletephoto.deletepoint=Effacer point attached to this photo?
+dialog.deleteduplicates.title=Effacer duplicates
+dialog.deleteduplicates.single.text=duplicate a Ã©té effacé
+dialog.deleteduplicates.multi.text=duplicates ont Ã©té effacés
 dialog.deleteduplicates.nonefound=No duplicates found
-dialog.compresstrack.title=Compress Track
+dialog.compresstrack.title=Comprimer 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.compresstrack.single.text=point a Ã©té effacé
+dialog.compresstrack.multi.text=points ont Ã©té effacés
+dialog.compresstrack.nonefound=Pas de données ont Ã©té effacés
+dialog.openoptions.title=Ouvrir options
 dialog.openoptions.filesnippet=Extract of fichier
-dialog.load.table.field=Field
-dialog.load.table.datatype=Data Type
+dialog.load.table.field=Champ
+dialog.load.table.datatype=Typ des données
 dialog.load.table.description=Description
 dialog.delimiter.label=Séparateur de texte
 dialog.delimiter.comma=Virgule ,
@@ -87,23 +91,28 @@ 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.field=Champ
+dialog.save.table.hasdata=A information
 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.timestampformat=Format de timestamp
 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.text=Titre pour le data
+dialog.exportkml.altitude=Include altitudes (pour aviation)
+dialog.exportkml.kmz=Comprimer Ã  kmz fichier
+dialog.exportkml.exportimages=Export image thumbnails Ã  kmz
 dialog.exportkml.filetype=Classeur KML, KMZ
+dialog.exportgpx.title=Exporter au GPX
+dialog.exportgpx.name=Nom
+dialog.exportgpx.desc=Légende 
+dialog.exportgpx.filetype=Classeur GPX
 dialog.exportpov.title=Exporter au POV
 dialog.exportpov.text=Please enter the parameters for the POV export
 dialog.exportpov.font=Police
@@ -141,15 +150,41 @@ 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.photoname=Nom de photo
 dialog.saveexif.table.status=Status
-dialog.saveexif.table.save=Save
+dialog.saveexif.table.save=Enregistrer
 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.correlate.title=Correlate photos
+dialog.correlate.notimestamps=Les points n'ont pas de timestamps, donc ce n'est pas possible de correler.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated photos.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated photos to use as the time offset
+dialog.correlate.photoselect.photoname=Nom de photo
+dialog.correlate.photoselect.timediff=Difference de temps
+dialog.correlate.photoselect.photolater=Photo plus tard
+dialog.correlate.options.tip=Tip: By manually correlating at least one photo, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Offset de temps
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=heures,
+dialog.correlate.options.offset.minutes=minutes et
+dialog.correlate.options.offset.seconds=secondes
+dialog.correlate.options.photolater=Photo later than point
+dialog.correlate.options.pointlater=Point later than photo
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All photos are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one photo.
+dialog.correlate.confirmsingle.text=photo was correlated
+dialog.correlate.confirmmultiple.text=photos were correlated
+dialog.help.help=Consultez la page\n http://activityworkshop.net/software/prune/\npour de plus détails et user guides.
 dialog.about.title=À propos de Prune
 dialog.about.version=Version
 dialog.about.build=Build
@@ -160,19 +195,20 @@ dialog.about.translatedby=Texte en fran
 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.systeminfo.java3d=Java3d installé
+dialog.about.systeminfo.povray=Povray installé
+dialog.about.systeminfo.exiftool=Exiftool installé
 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=Crédits
+dialog.about.credits.code=Prune code Ã©crit par
+dialog.about.credits.exifcode=Exif code par
 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
+dialog.about.credits.translators=Interprète
+dialog.about.credits.translations=Traduction avec l'aide de
+dialog.about.credits.devtools=Outils de développement
+dialog.about.credits.othertools=Autre outils
+dialog.about.credits.thanks=Merci Ã 
 
 # 3d window
 dialog.3d.title=Vue Trois-d de Prune
@@ -187,12 +223,10 @@ button.back=Retour
 button.next=Prochain
 button.finish=Fini
 button.cancel=Annuler
-button.overwrite=Overwrite
+button.overwrite=Écraser
 button.moveup=Move up
 button.movedown=Move down
-button.deletepoint=Delete point
-button.deleterange=Delete range
-button.showlines=Show lines
+button.showlines=Montrer lignes
 button.edit=Éditer
 button.exit=Terminer
 button.close=Fermer
@@ -203,27 +237,30 @@ button.yestoall=Oui pour tous
 button.notoall=Non pour tous
 button.selectall=Sélecter tous
 button.selectnone=Sélecter rien
+button.preview=Preview
+button.guessfields=Guess fields
 
 # Display components
-display.nodata=No data loaded
+display.nodata=Pas de data loaded
 display.noaltitudes=Track data does not include altitudes
-details.trackdetails=Track details
-details.notrack=No track loaded
+details.trackdetails=Détails de track
+details.notrack=Pas de track loaded
 details.track.points=Points
-details.track.file=File
-details.track.numfiles=Number of fichiers
-details.pointdetails=Point details
+details.track.file=Fichier
+details.track.numfiles=Nombre de fichiers
+details.pointdetails=Détails de point
 details.index.selected=Index
 details.index.of=de
-details.nopointselection=No point selected
+details.nopointselection=Pas de point choisis
 details.photofile=Photo fichier
-details.norangeselection=No range selected
+details.norangeselection=No range choisis
 details.rangedetails=Range details
-details.range.selected=Selected
+details.range.selected=Choisis
 details.range.to=à
 details.altitude.to=à
 details.range.climb=Montée
 details.range.descent=Descente
+details.coordformat=Coordinate format
 details.distanceunits=Unités de distance
 display.range.time.secs=s
 display.range.time.mins=m
@@ -231,9 +268,10 @@ 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
+details.photodetails=Détails de photo
+details.nophoto=Pas de photo
+details.photo.loading=Charger
+details.photo.connected=Connected
 
 # Field names
 fieldname.latitude=Latitude
@@ -249,6 +287,8 @@ fieldname.distance=Distance
 fieldname.duration=Durée
 
 # Measurement units
+units.original=Original
+units.default=Default
 units.metres=mètres
 units.metres.short=m
 units.feet=pieds
@@ -260,6 +300,7 @@ units.miles.short=li
 units.degminsec=Deg-min-sec
 units.degmin=Deg-min
 units.deg=Degrés
+units.iso8601=ISO 8601
 
 # Cardinals for 3d plots
 cardinal.n=N
@@ -280,6 +321,8 @@ undo.deleteduplicates=delete duplicates
 undo.reverse=reverse range
 undo.rearrangewaypoints=rearrange waypoints
 undo.connectphoto=connect photo
+undo.disconnectphoto=disconnect photo
+undo.correlate=correlate photos
 
 # Error messages
 error.save.dialogtitle=Error saving data
diff --git a/tim/prune/lang/prune-texts_pl.properties b/tim/prune/lang/prune-texts_pl.properties
new file mode 100644 (file)
index 0000000..7524363
--- /dev/null
@@ -0,0 +1,352 @@
+# Text entries for the Prune application
+# Polish entries as extra
+
+# Menu entries
+menu.file=Plik
+menu.file.open=Otw\u00F3rz
+menu.file.addphotos=Dodaj zdj\u0119cia
+menu.file.save=Zapisz
+menu.file.exportkml=Eksportuj jako KML
+menu.file.exportgpx=Eksportuj jako GPX
+menu.file.exportpov=Eksportuj jako POV
+menu.file.exit=Zako\u0144cz
+menu.edit=Edycja
+menu.edit.undo=Cofnij
+menu.edit.clearundo=Wyczy\u015B\u0107 list\u0119 zmian
+menu.edit.editpoint=Edytuj punkt
+menu.edit.editwaypointname=Zmie\u0144 nazw\u0119 punktu po\u015Bredniego
+menu.edit.deletepoint=Usu\u0144 punkt
+menu.edit.deleterange=Usu\u0144 zakres
+menu.edit.deleteduplicates=Usu\u0144 duplikaty
+menu.edit.compress=Skompresuj scie\u017Ck\u0119
+menu.edit.interpolate=Interpoluj punkty
+menu.edit.reverse=Odwr\u00F3\u0107 zakres
+menu.edit.rearrange=Zmie\u0144 kolejno\u015B\u0107 punkt\u00F3w po\u015Brednich
+menu.edit.rearrange.start=Wszystkie na pocz\u0105tek \u015Bcie\u017Cki
+menu.edit.rearrange.end=Wszystkie na koniec \u015Bcie\u017Cki
+menu.edit.rearrange.nearest=Do najbli\u017Cszego punktu
+menu.select=Zakres
+menu.select.all=Zaznacz wszystko
+menu.select.none=Usu\u0144 zaznaczenie
+menu.select.start=Zaznacz pocz\u0105tek
+menu.select.end=Zaznacz koniec
+menu.photo=Zdj\u0119cie
+menu.photo.saveexif=Zapisz Exif
+menu.photo.connect=Przy\u0142\u0105cz do punktu
+menu.photo.disconnect=Od\u0142\u0105cz od punktu
+menu.photo.correlate=Skoreluj wszystkie zdj\u0119cia
+menu.photo.delete=Usu\u0144 zdj\u0119cie
+menu.3d=Operacje 3D
+menu.3d.show3d=Poka\u017C model
+menu.help=Pomoc
+menu.help.about=Prune - Informacje
+# Popup menu for map
+menu.map.zoomin=Powi\u0119ksz
+menu.map.zoomout=Zmniejsz
+menu.map.zoomfull=Dostosuj powi\u0119kszenie
+menu.map.connect=Connect track punkty
+menu.map.autopan=Autopan
+
+# Dialogs
+dialog.exit.confirm.title=Zako\u0144cz 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=Usu\u0144 punkt
+dialog.deletepoint.deletephoto=Usu\u0144 zdj\u0119cie attached to this punkt?
+dialog.deletephoto.title=Usu\u0144 zdj\u0119cie
+dialog.deletephoto.deletepoint=Usu\u0144 punkt attached to this zdj\u0119cie?
+dialog.deleteduplicates.title=Usu\u0144 Duplicates
+dialog.deleteduplicates.single.text=duplicate was deleted
+dialog.deleteduplicates.multi.text=duplicates were deleted
+dialog.deleteduplicates.nonefound=Brak duplikaty found
+dialog.compresstrack.title=Skompresuj scie\u017Ck\u0119
+dialog.compresstrack.parameter.text=Parameter for compression (lower number = more compression)
+dialog.compresstrack.text=Track compressed
+dialog.compresstrack.single.text=data punkt was removed
+dialog.compresstrack.multi.text=data punkty were removed
+dialog.compresstrack.nonefound=No data punkty could be removed
+dialog.openoptions.title=Otw\u00F3rz opcje
+dialog.openoptions.filesnippet=Extract of plik
+dialog.load.table.field=Pole
+dialog.load.table.datatype=Data Type
+dialog.load.table.description=Opis
+dialog.delimiter.label=Pole separator
+dialog.delimiter.comma=Przecinek ,
+dialog.delimiter.tab=Tabulator
+dialog.delimiter.space=Spacja
+dialog.delimiter.semicolon=\u015Arednik ;
+dialog.delimiter.other=Inne
+dialog.openoptions.deliminfo.records=records, with 
+dialog.openoptions.deliminfo.fields=pola
+dialog.openoptions.deliminfo.norecords=No records
+dialog.openoptions.tabledesc=Extract of plik
+dialog.openoptions.altitudeunits=Wysoko\u015B\u0107 units
+dialog.jpegload.subdirectories=Include subdirectories
+dialog.jpegload.loadjpegswithoutcoords=Include zdj\u0119cia without coordinates
+dialog.jpegload.progress.title=Loading zdj\u0119cia
+dialog.jpegload.progress=Please wait while the zdj\u0119cia are searched
+dialog.jpegload.title=Loaded zdj\u0119cia
+dialog.jpegload.photoadded=zdj\u0119cie was added
+dialog.jpegload.photosadded=zdj\u0119cia were added
+dialog.saveoptions.title=Zapisz plik
+dialog.save.fieldstosave=Pola to save
+dialog.save.table.field=Pole
+dialog.save.table.hasdata=Has data
+dialog.save.table.save=Zapisz
+dialog.save.headerrow=Output header row
+dialog.save.coordinateunits=Wsp\u00f3\u0142rz\u0119dne units
+dialog.save.altitudeunits=Wysoko\u015B\u0107 units
+dialog.save.timestampformat=Timestamp format
+dialog.save.oktitle=Plik saved
+dialog.save.ok1=Successfully saved
+dialog.save.ok2=punkty to plik
+dialog.save.overwrite.title=Plik ju\u017C istnieje
+dialog.save.overwrite.text=This plik already exists. Are you sure you want to overwrite the plik?
+dialog.exportkml.title=Eksportuj KML
+dialog.exportkml.text=Tytu\u0142 for the data
+dialog.exportkml.altitude=Include altitudes (for aviation)
+dialog.exportkml.kmz=Compress to make kmz plik
+dialog.exportkml.exportimages=Eksportuj image thumbnails to kmz
+dialog.exportkml.filetype=KML, KMZ pliki
+dialog.exportgpx.title=Eksportuj GPX
+dialog.exportgpx.name=Nazwa
+dialog.exportgpx.desc=Opis
+dialog.exportgpx.filetype=GPX pliki
+dialog.exportpov.title=Eksportuj POV
+dialog.exportpov.text=Please enter the parameters for the POV export
+dialog.exportpov.font=Czcionka
+dialog.exportpov.camerax=Camera X
+dialog.exportpov.cameray=Camera Y
+dialog.exportpov.cameraz=Camera Z
+dialog.exportpov.filetype=POV pliki
+dialog.exportpov.warningtracksize=This track has a large number of punkty, which Java3D might not be able to display.\nCzy chcesz kontynuowa\u0107?
+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=Interpoluj punkty
+dialog.interpolate.parameter.text=Number of punkty to insert between selected punkty
+dialog.undo.title=Cofnij 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=Wyczy\u015B\u0107 list\u0119 zmian
+dialog.clearundo.text=Are you sure you want to clear the undo list?\nAll undo information will be lost!
+dialog.pointedit.title=Edytuj punkt
+dialog.pointedit.text=Select each field to edit and use the 'Edit' button to change the value
+dialog.pointedit.table.field=Pole
+dialog.pointedit.table.value=Value
+dialog.pointedit.table.changed=Zmieniony
+dialog.pointedit.changevalue.text=Enter the new value for this field
+dialog.pointedit.changevalue.title=Edytuj field
+dialog.pointnameedit.title=Zmie\u0144 nazw\u0119 punktu po\u015Bredniego
+dialog.pointnameedit.name=Waypoint nazwa
+dialog.pointnameedit.uppercase=UPPER case
+dialog.pointnameedit.lowercase=lower case
+dialog.pointnameedit.sentencecase=Sentence case
+dialog.saveexif.title=Zapisz Exif
+dialog.saveexif.intro=Select the zdj\u0119cia 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=Nazwa zdj\u0119cie
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Zapisz
+dialog.saveexif.photostatus.connected=Connected
+dialog.saveexif.photostatus.disconnected=Disconnected
+dialog.saveexif.photostatus.modified=Modified
+dialog.saveexif.overwrite=Overwrite pliki
+dialog.saveexif.ok1=Saved
+dialog.saveexif.ok2=zdj\u0119cia pliki
+dialog.correlate.title=Skoreluj zdj\u0119cie
+dialog.correlate.notimestamps=There are no timestamps in the data punkty, so there is nothing to correlate with the zdj\u0119cia.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated zdj\u0119cia.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated zdj\u0119cia to use as the time offset
+dialog.correlate.photoselect.photoname=Nazwa zdj\u0119cie
+dialog.correlate.photoselect.timediff=Time difference
+dialog.correlate.photoselect.photolater=Zdj\u0119cie later
+dialog.correlate.options.tip=Tip: By manually correlating at least one zdj\u0119cie, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Time offset
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=hours,
+dialog.correlate.options.offset.minutes=minuty i
+dialog.correlate.options.offset.seconds=seconds
+dialog.correlate.options.photolater=Zdj\u0119cie po punkt
+dialog.correlate.options.pointlater=Punkt po zdj\u0119cie
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All zdj\u0119cia are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one zdj\u0119cie.
+dialog.correlate.confirmsingle.text=zdj\u0119cie was correlated
+dialog.correlate.confirmmultiple.text=zdj\u0119cia were correlated
+dialog.help.help=Please see\n http://activityworkshop.net/software/prune/\nfor more information and user guides.
+dialog.about.title=Prune Informacje
+dialog.about.version=Wersja
+dialog.about.build=Build
+dialog.about.summarytext1=Prune is a program 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=Please see <code style="font-weight:bold">http://activityworkshop.net/</code> for more information and user guides.
+dialog.about.translatedby=Tekst po polsku by Piotr.
+dialog.about.systeminfo=System info
+dialog.about.systeminfo.os=Operating System
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d zainstalowana
+dialog.about.systeminfo.povray=Povray zainstalowana
+dialog.about.systeminfo.exiftool=Exiftool zainstalowana
+dialog.about.yes=Tak
+dialog.about.no=Nie
+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.translators=Translators
+dialog.about.credits.translations=T\u0142umaczenie helped by
+dialog.about.credits.devtools=Development tools
+dialog.about.credits.othertools=Other tools
+dialog.about.credits.thanks=Dzi\u0119kuje to
+
+# 3d window
+dialog.3d.title=Prune tr\u00f3jwymiarowa model
+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=Poprzedni
+button.next=Nast\u0119pny
+button.finish=Finish
+button.cancel=Anuluj
+button.overwrite=Overwrite
+button.moveup=Do g\u00F3ry
+button.movedown=Move down
+button.showlines=Show lines
+button.edit=Edycja
+button.exit=Zako\u0144cz
+button.close=Zamknij
+button.continue=Continue
+button.yes=Tak
+button.no=Nie
+button.yestoall=Tak to all
+button.notoall=Nie to all
+button.selectall=Select all
+button.selectnone=Select none
+button.preview=Preview
+button.guessfields=Guess fields
+
+# Display components
+display.nodata=No data loaded
+display.noaltitudes=Track data does not include altitudes
+details.trackdetails=Track szczeg\u00F3\u0142y
+details.notrack=No track loaded
+details.track.points=Punkty
+details.track.file=Plik
+details.track.numfiles=Number ze pliki
+details.pointdetails=Punkt szczeg\u00F3\u0142y
+details.index.selected=Index
+details.index.of=of
+details.nopointselection=No punkt selected
+details.photofile=Plik zdj\u0119cie
+details.norangeselection=No range selected
+details.rangedetails=Range szczeg\u00F3\u0142y
+details.range.selected=Selected
+details.range.to=to
+details.altitude.to=to
+details.range.climb=Climb
+details.range.descent=Descent
+details.coordformat=Wsp\u00f3\u0142rz\u0119dne format
+details.distanceunits=Distance units
+display.range.time.secs=s
+display.range.time.mins=m
+display.range.time.hours=h
+display.range.time.days=d
+details.waypointsphotos.waypoints=Waypoints
+details.waypointsphotos.photos=Zdj\u0119cia
+details.photodetails=Zdj\u0119cie szczeg\u00F3\u0142y
+details.nophoto=No zdj\u0119cie selected
+details.photo.loading=Wczytywanie
+details.photo.connected=Connected
+
+# Field names
+fieldname.latitude=Szeroko\u015B\u0107
+fieldname.longitude=D\u0142ugo\u015B\u0107
+fieldname.altitude=Wysoko\u015B\u0107
+fieldname.timestamp=Timestamp
+fieldname.waypointname=Nazwa
+fieldname.waypointtype=Type
+fieldname.newsegment=Segment
+fieldname.custom=U\u017Cytkownika
+fieldname.prefix=Pole
+fieldname.distance=Distance
+fieldname.duration=Duration
+
+# Measurement units
+units.original=Oryginalny
+units.default=Default
+units.metres=Metres
+units.metres.short=m
+units.feet=Feet
+units.feet.short=ft
+units.kilometres=Kilometres
+units.kilometres.short=km
+units.miles=Miles
+units.miles.short=mi
+units.degminsec=Deg-min-sek
+units.degmin=Deg-min
+units.deg=Degrees
+units.iso8601=ISO 8601
+
+# Cardinals for 3d plots
+cardinal.n=N
+cardinal.s=S
+cardinal.e=E
+cardinal.w=W
+
+# Undo operations
+undo.load=load data
+undo.loadphotos=load zdj\u0119cia
+undo.editpoint=edycja punkt
+undo.deletepoint=usu\u0144 punkt
+undo.deletephoto=remove zdj\u0119cie
+undo.deleterange=usu\u0144 range
+undo.compress=compress track
+undo.insert=insert punkty
+undo.deleteduplicates=usu\u0144 duplicates
+undo.reverse=reverse range
+undo.rearrangewaypoints=rearrange waypoints
+undo.connectphoto=connect zdj\u0119cie
+undo.disconnectphoto=disconnect zdj\u0119cie
+undo.correlate=correlate zdj\u0119cia
+
+# Error messages
+error.save.dialogtitle=Error saving data
+error.save.nodata=No data to save
+error.save.failed=Failed to save the data to plik:
+error.saveexif.filenotfound=Failed to find zdj\u0119cie plik
+error.saveexif.cannotoverwrite1=Zdj\u0119cie plik
+error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy?
+error.load.dialogtitle=B\u0142\u0105d loading data
+error.load.noread=Cannot read plik
+error.load.nopoints=No coordinate information found in the plik
+error.load.unknownxml=Nieznany xml format:
+error.load.othererror=B\u0142\u0105d reading plik:
+error.jpegload.dialogtitle=B\u0142\u0105d loading zdj\u0119cia
+error.jpegload.nofilesfound=No pliki found
+error.jpegload.nojpegsfound=No jpeg pliki 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=Sorry, 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=B\u0142\u0105d in 3d display
+error.3d=A b\u0142\u0105d occurred with the 3d display
index d882972969e8b5f3c516c47104b5ee60bec9d1bc..0bb6b733fb8f2faaf1ba342b2c04d065611d5087 100644 (file)
@@ -21,41 +21,50 @@ public class DelimiterInfo
                _delimiter = inChar;
        }
 
+       /** @return the delimiter character */
        public char getDelimiter()
        {
                return _delimiter;
        }
 
+       /** @return the max number of fields */
        public int getMaxFields()
        {
                return _maxFields;
        }
 
-       public void updateMaxFields(int inNumields)
+       /** @param inNumFields number of fields */
+       public void updateMaxFields(int inNumFields)
        {
-               if (inNumields > _maxFields)
-                       _maxFields = inNumields;
+               if (inNumFields > _maxFields)
+                       _maxFields = inNumFields;
        }
 
-
+       /** @return the number of records */
        public int getNumRecords()
        {
                return _numRecords;
        }
+
+       /** Increment the number of records */
        public void incrementNumRecords()
        {
                _numRecords++;
        }
 
+       /** @return the number of times this delimiter has won */
        public int getNumWinningRecords()
        {
                return _numWinningRecords;
        }
+
+       /** Increment the number of times this delimiter has won */
        public void incrementNumWinningRecords()
        {
                _numWinningRecords++;
        }
 
+       /** @return String for debug */
        public String toString()
        {
                return "(delim:" + _delimiter + " fields:" + _maxFields + ", records:" + _numRecords + ")";
diff --git a/tim/prune/load/FieldGuesser.java b/tim/prune/load/FieldGuesser.java
new file mode 100644 (file)
index 0000000..032874a
--- /dev/null
@@ -0,0 +1,273 @@
+package tim.prune.load;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Field;
+import tim.prune.data.Latitude;
+import tim.prune.data.Longitude;
+import tim.prune.data.Timestamp;
+
+/**
+ * Class to try to match data with field names,
+ * using a variety of guessing techniques
+ */
+public abstract class FieldGuesser
+{
+       /**
+        * Try to guess whether the given line is a header line or data
+        * @param inValues array of values from first non-blank line of file
+        * @return true if it looks like a header row, false if it looks like data
+        */
+       private static boolean isHeaderRow(String[] inValues)
+       {
+               // Loop over values looking for a Latitude value
+               if (inValues != null)
+               {
+                       for (int v=0; v<inValues.length; v++)
+                       {
+                               Latitude lat = new Latitude(inValues[v]);
+                               if (lat.isValid()) {return false;}
+                       }
+               }
+               // No valid Latitude value found so presume header
+               return true;
+       }
+
+
+       /**
+        * Try to guess the fields for the given values from the file
+        * @param inValues array of values from first non-blank line of file
+        * @return array of fields which hopefully match
+        */
+       public static Field[] guessFields(String[] inValues)
+       {
+               // Guess whether it's a header line or not
+               boolean isHeader = isHeaderRow(inValues);
+               // make array of Fields
+               int numFields = inValues.length;
+               Field[] fields = new Field[numFields];
+               // Loop over fields to try to guess the main ones
+               for (int f=0; f<numFields; f++)
+               {
+                       if (inValues[f] != null) {
+                               String value = inValues[f].trim();
+                               // check for latitude
+                               if (!checkArrayHasField(fields, Field.LATITUDE) && fieldLooksLikeLatitude(value, isHeader))
+                               {
+                                       fields[f] = Field.LATITUDE;
+                                       continue;
+                               }
+                               // check for longitude
+                               if (!checkArrayHasField(fields, Field.LONGITUDE) && fieldLooksLikeLongitude(value, isHeader))
+                               {
+                                       fields[f] = Field.LONGITUDE;
+                                       continue;
+                               }
+                               // check for altitude
+                               if (!checkArrayHasField(fields, Field.ALTITUDE) && fieldLooksLikeAltitude(value, isHeader))
+                               {
+                                       fields[f] = Field.ALTITUDE;
+                                       continue;
+                               }
+                               // check for waypoint name
+                               if (!checkArrayHasField(fields, Field.WAYPT_NAME) && fieldLooksLikeName(value, isHeader))
+                               {
+                                       fields[f] = Field.WAYPT_NAME;
+                                       continue;
+                               }
+                               // check for timestamp
+                               if (!checkArrayHasField(fields, Field.TIMESTAMP) && fieldLooksLikeTimestamp(value, isHeader))
+                               {
+                                       fields[f] = Field.TIMESTAMP;
+                                       continue;
+                               }
+                       }
+               }
+               // Fill in the rest of the fields using just custom fields
+               // Could try to guess other fields (waypoint type, segment) or unguessed altitude, name, but keep simple for now
+               String customPrefix = I18nManager.getText("fieldname.prefix") + " ";
+               int customFieldNum = 0;
+               for (int f=0; f<numFields; f++) {
+                       if (fields[f] == null)
+                       {
+                               // Make sure lat and long are filled in if not already
+                               if (!checkArrayHasField(fields, Field.LATITUDE)) {
+                                       fields[f] = Field.LATITUDE;
+                               }
+                               else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
+                                       fields[f] = Field.LONGITUDE;
+                               }
+                               else {
+                                       customFieldNum++;
+                                       fields[f] = new Field(customPrefix + (customFieldNum));
+                               }
+                       }
+               }
+               // Do a final check to make sure lat and long are in there
+               if (!checkArrayHasField(fields, Field.LATITUDE)) {
+                       fields[0] = Field.LATITUDE;
+               }
+               else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
+                       fields[1] = Field.LONGITUDE;
+               }
+               return fields;
+       }
+
+
+       /**
+        * Check whether the given field array has the specified field
+        * @param inFields
+        * @param inCheckField
+        * @return true if Field is contained within the array
+        */
+       private static boolean checkArrayHasField(Field[] inFields, Field inCheckField)
+       {
+               for (int f=0; f<inFields.length; f++)
+               {
+                       if (inFields[f] != null && inFields[f].equals(inCheckField)) {
+                               return true;
+                       }
+               }
+               // not found
+               return false;
+       }
+
+
+       /**
+        * Check whether the given String looks like a Latitude value
+        * @param inValue value from file
+        * @param inIsHeader true if this is a header line, false for data
+        * @return true if it could be latitude
+        */
+       private static boolean fieldLooksLikeLatitude(String inValue, boolean inIsHeader)
+       {
+               if (inValue == null || inValue.equals("")) {return false;}
+               if (inIsHeader)
+               {
+                       // This is a header line so look for english or local text
+                       String upperValue = inValue.toUpperCase();
+                       return (upperValue.equals("LATITUDE")
+                               || upperValue.equals(I18nManager.getText("fieldname.latitude").toUpperCase()));
+               }
+               else
+               {
+                       // Note this will also catch longitudes too
+                       Latitude lat = new Latitude(inValue);
+                       return lat.isValid();
+               }
+       }
+
+       /**
+        * Check whether the given String looks like a Longitude value
+        * @param inValue value from file
+        * @param inIsHeader true if this is a header line, false for data
+        * @return true if it could be longitude
+        */
+       private static boolean fieldLooksLikeLongitude(String inValue, boolean inIsHeader)
+       {
+               if (inValue == null || inValue.equals("")) {return false;}
+               if (inIsHeader)
+               {
+                       // This is a header line so look for english or local text
+                       String upperValue = inValue.toUpperCase();
+                       return (upperValue.equals("LONGITUDE")
+                               || upperValue.equals(I18nManager.getText("fieldname.longitude").toUpperCase()));
+               }
+               else
+               {
+                       // Note this will also catch latitudes too
+                       Longitude lon = new Longitude(inValue);
+                       return lon.isValid();
+               }
+       }
+
+       /**
+        * Check whether the given String looks like an Altitude value
+        * @param inValue value from file
+        * @param inIsHeader true if this is a header line, false for data
+        * @return true if it could be altitude
+        */
+       private static boolean fieldLooksLikeAltitude(String inValue, boolean inIsHeader)
+       {
+               if (inValue == null || inValue.equals("")) {return false;}
+               if (inIsHeader)
+               {
+                       // This is a header line so look for english or local text
+                       String upperValue = inValue.toUpperCase();
+                       return (upperValue.equals("ALTITUDE")
+                               || upperValue.equals("ALT")
+                               || upperValue.equals(I18nManager.getText("fieldname.altitude").toUpperCase()));
+               }
+               else
+               {
+                       // Look for a number less than 100000
+                       try
+                       {
+                               int intValue = Integer.parseInt(inValue);
+                               return (intValue > 0 && intValue < 100000);
+                       }
+                       catch (NumberFormatException nfe) {}
+                       return false;
+               }
+       }
+
+
+       /**
+        * Check whether the given String looks like a waypoint name
+        * @param inValue value from file
+        * @param inIsHeader true if this is a header line, false for data
+        * @return true if it could be a name
+        */
+       private static boolean fieldLooksLikeName(String inValue, boolean inIsHeader)
+       {
+               if (inValue == null || inValue.equals("")) {return false;}
+               if (inIsHeader)
+               {
+                       // This is a header line so look for english or local text
+                       String upperValue = inValue.toUpperCase();
+                       return (upperValue.equals("NAME")
+                               || upperValue.equals("LABEL")
+                               || upperValue.equals(I18nManager.getText("fieldname.waypointname").toUpperCase()));
+               }
+               else
+               {
+                       // Look for at least two letters in it
+                       int numLetters = 0;
+                       for (int i=0; i<inValue.length(); i++)
+                       {
+                               char currChar = inValue.charAt(i);
+                               if (Character.isLetter(currChar)) {
+                                       numLetters++;
+                               }
+                               // Not interested if it contains ":" or "."
+                               if (currChar == ':' || currChar == '.') {return false;}
+                       }
+                       return numLetters >= 2;
+               }
+       }
+
+       /**
+        * Check whether the given String looks like a timestamp
+        * @param inValue value from file
+        * @param inIsHeader true if this is a header line, false for data
+        * @return true if it could be a timestamp
+        */
+       private static boolean fieldLooksLikeTimestamp(String inValue, boolean inIsHeader)
+       {
+               if (inValue == null || inValue.equals("")) {return false;}
+               if (inIsHeader)
+               {
+                       String upperValue = inValue.toUpperCase();
+                       // This is a header line so look for english or local text
+                       return (upperValue.equals("TIMESTAMP")
+                               || upperValue.equals("TIME")
+                               || upperValue.equals(I18nManager.getText("fieldname.timestamp").toUpperCase()));
+               }
+               else
+               {
+                       // must be at least 7 characters long
+                       if (inValue.length() < 7) {return false;}
+                       Timestamp stamp = new Timestamp(inValue);
+                       return stamp.isValid();
+               }
+       }
+}
index 11b60d9d8c072e02bb380c10041b44c020524a56..6814e2b093df8aec10b893076cd539287377cdbc 100644 (file)
@@ -27,7 +27,7 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
 
        /**
-        * Get the column count
+        * @return the column count
         */
        public int getColumnCount()
        {
@@ -36,7 +36,8 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
 
        /**
-        * Get the name of the column
+        * @param inColNum column number
+        * @return name of the column
         */
        public String getColumnName(int inColNum)
        {
@@ -47,7 +48,7 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
 
        /**
-        * Get the row count
+        * @return the row count
         */
        public int getRowCount()
        {
@@ -58,14 +59,16 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
 
        /**
-        * Get the value of the specified cell
+        * @param inRowIndex row index
+        * @param inColumnIndex column index
+        * @return the value of the specified cell
         */
-       public Object getValueAt(int rowIndex, int columnIndex)
+       public Object getValueAt(int inRowIndex, int inColumnIndex)
        {
                if (_fieldArray == null) return "";
-               if (columnIndex == 0) return ("" + (rowIndex+1));
-               Field field = _fieldArray[rowIndex];
-               if (columnIndex == 1)
+               if (inColumnIndex == 0) return ("" + (inRowIndex+1));
+               Field field = _fieldArray[inRowIndex];
+               if (inColumnIndex == 1)
                {
                        // Field name - take name from built-in fields
                        if (field.isBuiltIn())
@@ -81,13 +84,16 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
        /**
         * Make sure only second and third columns are editable
+        * @param inRowIndex row index
+        * @param inColumnIndex column index
+        * @return true if cell editable
         */
-       public boolean isCellEditable(int rowIndex, int columnIndex)
+       public boolean isCellEditable(int inRowIndex, int inColumnIndex)
        {
-               if (columnIndex <= 1)
-                       return (columnIndex == 1);
+               if (inColumnIndex <= 1)
+                       return (inColumnIndex == 1);
                // Column is 2 so only edit non-builtin field names
-               Field field = _fieldArray[rowIndex];
+               Field field = _fieldArray[inRowIndex];
                return !field.isBuiltIn();
        }
 
@@ -109,24 +115,27 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
        /**
         * React to edits to the table data
+        * @param inValue value to set
+        * @param inRowIndex row index
+        * @param inColumnIndex column index
         */
-       public void setValueAt(Object aValue, int rowIndex, int columnIndex)
+       public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex)
        {
-               super.setValueAt(aValue, rowIndex, columnIndex);
-               if (columnIndex == 1)
+               super.setValueAt(inValue, inRowIndex, inColumnIndex);
+               if (inColumnIndex == 1)
                {
-                       Field field = _fieldArray[rowIndex];
-                       if (!field.getName().equals(aValue.toString()))
+                       Field field = _fieldArray[inRowIndex];
+                       if (!field.getName().equals(inValue.toString()))
                        {
-                               manageFieldChange(rowIndex, aValue.toString());
+                               manageFieldChange(inRowIndex, inValue.toString());
                        }
                }
-               else if (columnIndex == 2)
+               else if (inColumnIndex == 2)
                {
                        // change description if it's custom
-                       Field field = _fieldArray[rowIndex];
+                       Field field = _fieldArray[inRowIndex];
                        if (!field.isBuiltIn())
-                               field.setName(aValue.toString());
+                               field.setName(inValue.toString());
                }
        }
 
index ad0f0efb9088855696c898178bee335f25bfadaf..63c8b24eacfd5a4d9354e35f48a99f6794d9a8a4 100644 (file)
@@ -77,7 +77,8 @@ public class FileCacher
 
        /**
         * Get the top section of the file for preview
-        * @param inSize number of lines to extract
+        * @param inNumRows number of lines to extract
+        * @param inMaxWidth max length of Strings (longer ones will be chopped)
         * @return String array containing non-blank lines from the file
         */
        public String[] getSnippet(int inNumRows, int inMaxWidth)
index 216e027c16b2562e6bc52c75483021a30d4292c9..39cd36c4fd6d382850642069a3b588a1e6088bbf 100644 (file)
@@ -1,8 +1,5 @@
 package tim.prune.load;
 
-import tim.prune.I18nManager;
-import tim.prune.data.Field;
-
 /**
  * Class responsible for splitting the file contents into an array
  * based on the selected delimiter character
@@ -13,6 +10,8 @@ public class FileSplitter
        private int _numRows = 0;
        private int _numColumns = 0;
        private boolean[] _columnStates = null;
+       private String[] _firstFullRow = null;
+
 
        /**
         * Constructor
@@ -30,6 +29,7 @@ public class FileSplitter
         */
        public String[][] splitFieldData(char inDelim)
        {
+               _firstFullRow = null;
                if (_cacher == null) return null;
                String[] contents = _cacher.getContents();
                if (contents == null || contents.length == 0) return null;
@@ -46,6 +46,7 @@ public class FileSplitter
                                if (splitLine != null && splitLine.length > maxFields)
                                {
                                        maxFields = splitLine.length;
+                                       _firstFullRow = splitLine;
                                }
                        }
                }
@@ -100,6 +101,14 @@ public class FileSplitter
                return _numColumns;
        }
 
+       /**
+        * @return the fields in the first full row
+        */
+       public String[] getFirstFullRow()
+       {
+               return _firstFullRow; 
+       }
+
 
        /**
         * Check if the specified column of the data is blank
@@ -111,37 +120,4 @@ public class FileSplitter
                // Should probably trap out of range values
                return !_columnStates[inColumnNum];
        }
-
-
-       /**
-        * @return a Field array to use as defaults for the data
-        */
-       public Field[] makeDefaultFields()
-       {
-               Field[] fields = null;
-               if (_numColumns > 0)
-               {
-                       fields = new Field[_numColumns];
-                       try
-                       {
-                               fields[0] = Field.LATITUDE;
-                               fields[1] = Field.LONGITUDE;
-                               fields[2] = Field.ALTITUDE;
-                               fields[3] = Field.WAYPT_NAME;
-                               fields[4] = Field.WAYPT_TYPE;
-                               String customPrefix = I18nManager.getText("fieldname.prefix") + " ";
-                               for (int i=5;; i++)
-                               {
-                                       fields[i] = new Field(customPrefix + (i+1));
-                               }
-                       }
-                       catch (ArrayIndexOutOfBoundsException finished)
-                       {
-                               // Finished populating array
-                       }
-               }
-               else
-                       fields = new Field[0];
-               return fields;
-       }
 }
index e6917c1a48c2500f846e78cdd1338a97f000e3c0..a2453bec57a071d8e7abb4c85c1e4b623c31bf64 100644 (file)
@@ -3,7 +3,7 @@ package tim.prune.load;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.File;
-import java.util.ArrayList;
+import java.util.TreeSet;
 
 import javax.swing.BorderFactory;
 import javax.swing.BoxLayout;
@@ -45,7 +45,7 @@ public class JpegLoader implements Runnable
        private JProgressBar _progressBar = null;
        private int[] _fileCounts = null;
        private boolean _cancelled = false;
-       private ArrayList _photos = null;
+       private TreeSet _photos = null;
 
 
        /**
@@ -128,7 +128,7 @@ public class JpegLoader implements Runnable
        {
                // Initialise arrays, errors, summaries
                _fileCounts = new int[4]; // files, jpegs, exifs, gps
-               _photos = new ArrayList();
+               _photos = new TreeSet(new PhotoSorter());
                File[] files = _fileChooser.getSelectedFiles();
                // Loop recursively over selected files/directories to count files
                int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
@@ -244,9 +244,9 @@ public class JpegLoader implements Runnable
                                {_fileCounts[2]++;} // exif found
                        if (jpegData.isValid())
                        {
-                               if (jpegData.getDatestamp() != null && jpegData.getTimestamp() != null)
+                               if (jpegData.getGpsDatestamp() != null && jpegData.getGpsTimestamp() != null)
                                {
-                                       photo.setTimestamp(createTimestamp(jpegData.getDatestamp(), jpegData.getTimestamp()));
+                                       photo.setTimestamp(createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp()));
                                }
                                // Make DataPoint and attach to Photo
                                DataPoint point = createDataPoint(jpegData);
@@ -255,6 +255,12 @@ public class JpegLoader implements Runnable
                                photo.setOriginalStatus(PhotoStatus.TAGGED);
                                _fileCounts[3]++;
                        }
+                       // Use exif timestamp if gps timestamp not available
+                       if (photo.getTimestamp() == null && jpegData.getOriginalTimestamp() != null)
+                       {
+                               photo.setTimestamp(createTimestamp(jpegData.getOriginalTimestamp()));
+                       }
+                       photo.setExifThumbnail(jpegData.getThumbnailImage());
                }
                catch (JpegException jpe) { // don't list errors, just count them
                }
@@ -367,6 +373,28 @@ public class JpegLoader implements Runnable
        }
 
 
+       /**
+        * Use the given String value to create a timestamp
+        * @param inStamp timestamp from exif
+        * @return Timestamp object corresponding to input
+        */
+       private static Timestamp createTimestamp(String inStamp)
+       {
+               Timestamp stamp = null;
+               try
+               {
+                       stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
+                               Integer.parseInt(inStamp.substring(5, 7)),
+                               Integer.parseInt(inStamp.substring(8, 10)),
+                               Integer.parseInt(inStamp.substring(11, 13)),
+                               Integer.parseInt(inStamp.substring(14, 16)),
+                               Integer.parseInt(inStamp.substring(17)));
+               }
+               catch (NumberFormatException nfe) {}
+               return stamp;
+       }
+
+
        /**
         * Check whether to accept the given filename
         * @param inName name of file
diff --git a/tim/prune/load/PhotoMeasurer.java b/tim/prune/load/PhotoMeasurer.java
deleted file mode 100644 (file)
index 710508a..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-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/PhotoSorter.java b/tim/prune/load/PhotoSorter.java
new file mode 100644 (file)
index 0000000..7e65cac
--- /dev/null
@@ -0,0 +1,32 @@
+package tim.prune.load;
+
+import java.io.File;
+import java.util.Comparator;
+
+import tim.prune.data.Photo;
+
+/**
+ * Class to sort photos by name
+ */
+public class PhotoSorter implements Comparator
+{
+
+       /**
+        * Compare two photos
+        * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
+        */
+       public int compare(Object o1, Object o2)
+       {
+               File file1 = ((Photo) o1).getFile();
+               File file2 = ((Photo) o2).getFile();
+               int nameComp = file1.getName().compareTo(file2.getName());
+               if (nameComp == 0)
+               {
+                       // names same, maybe in different directories
+                       return file1.getAbsolutePath().compareTo(file2.getAbsolutePath());
+               }
+               // names different
+               return nameComp;
+       }
+
+}
index b6926311a60f1f79ff0ec024c3808ca247e4370f..977845ae597152002a58eeba164d25c4adf0dede 100644 (file)
@@ -99,8 +99,8 @@ public class TextFileLoader
 
 
        /**
-        * Open the selected file and show the GUI dialog
-        * to select load options
+        * Open the selected file and show the GUI dialog to select load options
+        * @param inFile file to open
         */
        public void openFile(File inFile)
        {
@@ -364,7 +364,16 @@ public class TextFileLoader
                        }
                });
                innerPanel3.add(_moveDownButton);
-               innerPanel3.add(Box.createVerticalStrut(70));
+               innerPanel3.add(Box.createVerticalStrut(60));
+               JButton guessButton = new JButton(I18nManager.getText("button.guessfields"));
+               guessButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _lastSelectedFields = null;
+                               prepareSecondPanel();
+                       }
+               });
+               innerPanel3.add(guessButton);
 
                innerPanel2.add(innerPanel3, BorderLayout.EAST);
                secondCard.add(innerPanel2, BorderLayout.CENTER);
@@ -458,11 +467,8 @@ public class TextFileLoader
                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());
+               String[][] tableData = splitter.splitFieldData(info.getDelimiter());
                // possible to ignore blank columns here
                _currentDelimiter = info.getDelimiter();
                _fileExtractTableModel.updateData(tableData);
@@ -471,9 +477,15 @@ public class TextFileLoader
                // 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();
+               {
+                       // Take first full row of file and use it to guess fields
+                       startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
+               }
+
                _fieldTableModel.updateData(startFieldArray);
                _fieldTable.setModel(_fieldTableModel);
                // add dropdowns to second column
index 962b495741909fb0e46c1fc07c62c254df18a3d1..771621b57f75b98ca1d5229704dda7b63e3a446b 100644 (file)
@@ -13,6 +13,7 @@ import tim.prune.data.Field;
  */
 public class GpxHandler extends XmlHandler
 {
+       private boolean _insideWaypoint = false;
        private boolean _insideName = false;
        private boolean _insideElevation = false;
        private boolean _insideTime = false;
@@ -27,11 +28,12 @@ public class GpxHandler extends XmlHandler
         * @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
+               Attributes attributes) throws SAXException
        {
                // Read the parameters for waypoints and track points
                if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
                {
+                       _insideWaypoint = qName.equalsIgnoreCase("wpt");
                        int numAttributes = attributes.getLength();
                        for (int i=0; i<numAttributes; i++)
                        {
@@ -64,7 +66,7 @@ public class GpxHandler extends XmlHandler
         * @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
+               throws SAXException
        {
                if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
                {
@@ -91,10 +93,10 @@ public class GpxHandler extends XmlHandler
         * @see org.xml.sax.ContentHandler#characters(char[], int, int)
         */
        public void characters(char[] ch, int start, int length)
-                       throws SAXException
+               throws SAXException
        {
                String value = new String(ch, start, length);
-               if (_insideName) {_name = checkCharacters(_name, value);}
+               if (_insideName && _insideWaypoint) {_name = checkCharacters(_name, value);}
                else if (_insideElevation) {_elevation = checkCharacters(_elevation, value);}
                else if (_insideTime) {_time = checkCharacters(_time, value);}
                super.characters(ch, start, length);
index a607b22a2803b1b0cc09708ab6b6ef0390443681..987841e6935cc4c680a34d5b7db2ac3c24dd8fe5 100644 (file)
@@ -39,8 +39,8 @@ public class XmlFileLoader extends DefaultHandler implements Runnable
 
 
        /**
-        * Open the selected file and show the GUI dialog
-        * to select load options
+        * Open the selected file and show the GUI dialog to select load options
+        * @param inFile File to open
         */
        public void openFile(File inFile)
        {
@@ -94,7 +94,7 @@ public class XmlFileLoader extends DefaultHandler implements Runnable
         * @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
+               Attributes attributes) throws SAXException
        {
                // Check for "kml" or "gpx" tags
                if (_handler == null)
@@ -120,7 +120,7 @@ public class XmlFileLoader extends DefaultHandler implements Runnable
         * @see org.xml.sax.ContentHandler#characters(char[], int, int)
         */
        public void characters(char[] ch, int start, int length)
-                       throws SAXException
+               throws SAXException
        {
                if (_handler != null)
                {
@@ -136,7 +136,7 @@ public class XmlFileLoader extends DefaultHandler implements Runnable
         * @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
+               throws SAXException
        {
                if (_handler != null)
                {
index 1f3143cf06085751001c938db47db65a00636c51..267378783e184130c308baad49e4197faa49e379 100644 (file)
@@ -1,7 +1,8 @@
-Prune version 3
+Prune version 4
 ===============
 
-Prune is an application for viewing, editing and managing coordinate data from GPS systems.
+Prune is an application for viewing, editing and managing coordinate data from GPS systems,
+including format conversion and photo correlation.
 
 Prune is copyright activityworkshop.net and distributed under the terms of the Gnu GPL version 2.
 You may freely use the software, and may help others to freely use it too.  For further information
@@ -15,16 +16,32 @@ Running
 =======
 
 To run Prune from the jar file, simply call it from a command prompt or shell:
-   java -jar prune_03.jar
+   java -jar prune_04.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
 in a file manager window to execute it.  A shortcut, menu item, desktop icon or other link
 can of course be made should you wish.
 
+To specify a language other than the default, use an additional parameter, eg:
+   java -jar prune_04.jar --lang=DE
 
-Updates since version 2
-=======================
+
+New with version 4
+==================
+
+The following features were added since version 3:
+  - Automatic correlation of photos with points based on timestamps
+  - Manual disconnection of photos from points
+  - Reading of photo thumbnails from exif data (speeds up photo loading)
+  - Export to GPX format
+  - KML and KMZ export now includes altitudes option for airborne tracks
+  - Track points in map can be connected by lines
+  - On loading a text file, fields are now guessed according to data or column headings
+  - Polish language
+
+New with version 3
+==================
 
 The following features were added since version 2:
   - Loading of GPX and KML files
@@ -33,15 +50,17 @@ The following features were added since version 2:
   - Saving of coordinates in exif data of jpegs
   - Exporting to KMZ format including thumbnails of photos
   - Four-panel layout with toolbar
+  - French language
 
-Updates since version 1
-=======================
+New with version 2
+==================
 
 The following features were added since version 1:
   - Display of data in 3d view using Java3D library
   - Export of 3d model to POV format for rendering by povray
   - Point edit dialog, waypoint name edit dialog
   - Waypoint list
+  - Spanish language
 
 
 Further information and updates
index 1e597201d5f2842197531159ec3452f542fb428c..ec0c2f446840359f2d9c7a24772c6bb7883ec96e 100644 (file)
@@ -72,6 +72,7 @@ public class ExifSaver implements Runnable
         * Save exif information to all photos in the list
         * whose coordinate information has changed since loading
         * @param inPhotoList list of photos to save
+        * @return true if saved
         */
        public boolean saveExifInformation(PhotoList inPhotoList)
        {
index 409854735678ef855e042a93c13933292c0a92ed..73bd466c924317158fc488916dfb9e6244590f59 100644 (file)
@@ -14,6 +14,8 @@ public class FieldInfo
 
        /**
         * Constructor
+        * @param inField Field object
+        * @param inData true if Field contains data which can be saved
         */
        public FieldInfo(Field inField, boolean inData)
        {
index 92fda1e540ff1eb84d17db2ae7dce425bb29a3d7..e4a27733aa5e6bad2292a720b8b892d6fc7db7b5 100644 (file)
@@ -14,6 +14,7 @@ public class FieldSelectionTableModel extends AbstractTableModel
 
        /**
         * Constructor giving list size
+        * @param inSize number of fields
         */
        public FieldSelectionTableModel(int inSize)
        {
@@ -61,9 +62,9 @@ public class FieldSelectionTableModel extends AbstractTableModel
                }
                else if (inColumnIndex == 1)
                {
-                       return new Boolean(_info[inRowIndex].hasData());
+                       return Boolean.valueOf(_info[inRowIndex].hasData());
                }
-               return new Boolean(_info[inRowIndex].isSelected());
+               return Boolean.valueOf(_info[inRowIndex].isSelected());
        }
 
 
index f5f92bcc91af9ba6aec94e26877400b0bc4598c2..db41a34aee5f581a1cd064e9e873f62d9c6f3dfc 100644 (file)
@@ -25,9 +25,11 @@ import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
 import javax.swing.JTable;
 import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
+import javax.swing.table.TableModel;
 
 import tim.prune.App;
 import tim.prune.I18nManager;
@@ -36,6 +38,7 @@ import tim.prune.data.Coordinate;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
 import tim.prune.data.FieldList;
+import tim.prune.data.Timestamp;
 import tim.prune.data.Track;
 import tim.prune.load.OneCharDocument;
 
@@ -55,14 +58,17 @@ public class FileSaver
        private JTable _table = null;
        private FieldSelectionTableModel _model = null;
        private JButton _moveUpButton = null, _moveDownButton = null;
+       private UpDownToggler _toggler = null;
        private JRadioButton[] _delimiterRadios = null;
        private JTextField _otherDelimiterText = null;
        private JCheckBox _headerRowCheckbox = null;
        private JRadioButton[] _coordUnitsRadios = null;
        private JRadioButton[] _altitudeUnitsRadios = null;
+       private JRadioButton[] _timestampUnitsRadios = null;
        private static final int[] FORMAT_COORDS = {Coordinate.FORMAT_NONE, Coordinate.FORMAT_DEG_MIN_SEC,
                Coordinate.FORMAT_DEG_MIN, Coordinate.FORMAT_DEG};
        private static final int[] FORMAT_ALTS = {Altitude.FORMAT_NONE, Altitude.FORMAT_METRES, Altitude.FORMAT_FEET};
+       private static final int[] FORMAT_TIMES = {Timestamp.FORMAT_ORIGINAL, Timestamp.FORMAT_LOCALE, Timestamp.FORMAT_ISO_8601};
 
 
        /**
@@ -85,8 +91,13 @@ public class FileSaver
         */
        public void showDialog(char inDefaultDelimiter)
        {
-               _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true);
-               _dialog.setLocationRelativeTo(_parentFrame);
+               if (_dialog == null)
+               {
+                       _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true);
+                       _dialog.setLocationRelativeTo(_parentFrame);
+                       _dialog.getContentPane().add(makeDialogComponents());
+                       _dialog.pack();
+               }
                // Check field list
                FieldList fieldList = _track.getFieldList();
                int numFields = fieldList.getNumFields();
@@ -97,19 +108,17 @@ public class FileSaver
                        FieldInfo info = new FieldInfo(field, _track.hasData(field));
                        _model.addFieldInfo(info, i);
                }
-               _dialog.getContentPane().add(makeDialogComponents(_model, inDefaultDelimiter));
-               _dialog.pack();
+               // Initialise dialog and show it
+               initDialog(_model, inDefaultDelimiter);
                _dialog.show();
        }
 
 
        /**
         * Make the dialog components
-        * @param inTableModel table model for fields
-        * @param inDelimiter default delimiter character
         * @return the GUI components for the save dialog
         */
-       private Component makeDialogComponents(FieldSelectionTableModel inTableModel, char inDelimiter)
+       private Component makeDialogComponents()
        {
                JPanel panel = new JPanel();
                panel.setLayout(new BorderLayout());
@@ -122,10 +131,12 @@ public class FileSaver
                firstCard.setLayout(new BoxLayout(firstCard, BoxLayout.Y_AXIS));
                JPanel tablePanel = new JPanel();
                tablePanel.setLayout(new BorderLayout());
-               _table = new JTable(inTableModel);
+               _table = new JTable();
                _table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-               tablePanel.add(_table.getTableHeader(), BorderLayout.NORTH);
-               tablePanel.add(_table, BorderLayout.CENTER);
+               // Enclose table in a scrollpane to prevent other components getting lost
+               JScrollPane scrollPane = new JScrollPane(_table);
+               _table.setPreferredScrollableViewportSize(new Dimension(300, 150));
+               tablePanel.add(scrollPane, BorderLayout.CENTER);
 
                // Make a panel to hold the table and up/down buttons
                JPanel fieldsPanel = new JPanel();
@@ -163,9 +174,8 @@ public class FileSaver
                updownPanel.add(_moveDownButton);
                fieldsPanel.add(updownPanel, BorderLayout.EAST);
                // enable/disable buttons based on table row selection
-               _table.getSelectionModel().addListSelectionListener(
-                       new UpDownToggler(_moveUpButton, _moveDownButton, inTableModel.getRowCount())
-               );
+               _toggler = new UpDownToggler(_moveUpButton, _moveDownButton);
+               _table.getSelectionModel().addListSelectionListener(_toggler);
 
                // Add fields panel and the delimiter panel to first card in pack
                JLabel saveOptionsLabel = new JLabel(I18nManager.getText("dialog.save.fieldstosave"));
@@ -204,16 +214,6 @@ public class FileSaver
                {
                        delimGroup.add(_delimiterRadios[i]);
                }
-               // choose last-used delimiter as default
-               switch (inDelimiter)
-               {
-                       case ','  : _delimiterRadios[0].setSelected(true); break;
-                       case '\t' : _delimiterRadios[1].setSelected(true); break;
-                       case ';'  : _delimiterRadios[2].setSelected(true); break;
-                       case ' '  : _delimiterRadios[3].setSelected(true); break;
-                       default   : _delimiterRadios[4].setSelected(true);
-                                               _otherDelimiterText.setText("" + inDelimiter);
-               }
                delimsPanel.add(otherPanel);
                firstCard.add(delimsPanel);
 
@@ -235,7 +235,7 @@ public class FileSaver
                coordsUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
                coordsUnitsPanel.setLayout(new GridLayout(0, 2));
                _coordUnitsRadios = new JRadioButton[4];
-               _coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original"));
+               _coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
                _coordUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.degminsec"));
                _coordUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.degmin"));
                _coordUnitsRadios[3] = new JRadioButton(I18nManager.getText("units.deg"));
@@ -249,6 +249,7 @@ public class FileSaver
                coordsUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
                secondCardHolder.add(coordsUnitsPanel);
                secondCardHolder.add(Box.createRigidArea(new Dimension(0,10)));
+               // altitude units
                JLabel altUnitsLabel = new JLabel(I18nManager.getText("dialog.save.altitudeunits"));
                altUnitsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
                secondCardHolder.add(altUnitsLabel);
@@ -256,7 +257,7 @@ public class FileSaver
                altUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
                altUnitsPanel.setLayout(new GridLayout(0, 2));
                _altitudeUnitsRadios = new JRadioButton[3];
-               _altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original"));
+               _altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
                _altitudeUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.metres"));
                _altitudeUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.feet"));
                ButtonGroup altGroup = new ButtonGroup();
@@ -268,7 +269,27 @@ public class FileSaver
                }
                altUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
                secondCardHolder.add(altUnitsPanel);
-               // TODO: selection of format of timestamps
+               secondCardHolder.add(Box.createRigidArea(new Dimension(0,10)));
+               // Selection of format of timestamps
+               JLabel timestampLabel = new JLabel(I18nManager.getText("dialog.save.timestampformat"));
+               timestampLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
+               secondCardHolder.add(timestampLabel);
+               JPanel timestampPanel = new JPanel();
+               timestampPanel.setBorder(BorderFactory.createEtchedBorder());
+               timestampPanel.setLayout(new GridLayout(0, 2));
+               _timestampUnitsRadios = new JRadioButton[3];
+               _timestampUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
+               _timestampUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.default"));
+               _timestampUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.iso8601"));
+               ButtonGroup timeGroup = new ButtonGroup();
+               for (int i=0; i<3; i++)
+               {
+                       timeGroup.add(_timestampUnitsRadios[i]);
+                       timestampPanel.add(_timestampUnitsRadios[i]);
+                       _timestampUnitsRadios[i].setSelected(i==0);
+               }
+               timestampPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
+               secondCardHolder.add(timestampPanel);
                secondCard.add(secondCardHolder, BorderLayout.NORTH);
                _cards.add(secondCard, "card2");
 
@@ -279,7 +300,7 @@ public class FileSaver
                _backButton.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
-                               CardLayout cl = (CardLayout)(_cards.getLayout());
+                               CardLayout cl = (CardLayout) _cards.getLayout();
                                cl.previous(_cards);
                                _backButton.setEnabled(false);
                                _nextButton.setEnabled(true);
@@ -292,7 +313,7 @@ public class FileSaver
                _nextButton.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e)
                        {
-                               CardLayout cl = (CardLayout)(_cards.getLayout());
+                               CardLayout cl = (CardLayout) _cards.getLayout();
                                cl.next(_cards);
                                _backButton.setEnabled(true);
                                _nextButton.setEnabled(false);
@@ -322,6 +343,34 @@ public class FileSaver
                return panel;
        }
 
+       /**
+        * Initialize the dialog with the given details
+        * @param inModel table model
+        * @param inDefaultDelimiter default delimiter character
+        */
+       private void initDialog(TableModel inModel, char inDefaultDelimiter)
+       {
+               // set table model
+               _table.setModel(inModel);
+               // reset toggler
+               _toggler.setListSize(inModel.getRowCount());
+               // choose last-used delimiter as default
+               switch (inDefaultDelimiter)
+               {
+                       case ','  : _delimiterRadios[0].setSelected(true); break;
+                       case '\t' : _delimiterRadios[1].setSelected(true); break;
+                       case ';'  : _delimiterRadios[2].setSelected(true); break;
+                       case ' '  : _delimiterRadios[3].setSelected(true); break;
+                       default   : _delimiterRadios[4].setSelected(true);
+                                               _otherDelimiterText.setText("" + inDefaultDelimiter);
+               }
+               // set card and enable buttons
+               CardLayout cl = (CardLayout) _cards.getLayout();
+               cl.first(_cards);
+               _nextButton.setEnabled(true);
+               _backButton.setEnabled(false);
+       }
+
 
        /**
         * Save the track to file with the chosen options
@@ -351,6 +400,15 @@ public class FileSaver
                                        altitudeFormat = FORMAT_ALTS[i];
                                }
                        }
+                       // Get timestamp formats
+                       int timestampFormat = Timestamp.FORMAT_ORIGINAL;
+                       for (int i=0; i<_timestampUnitsRadios.length; i++)
+                       {
+                               if (_timestampUnitsRadios[i].isSelected())
+                               {
+                                       timestampFormat = FORMAT_TIMES[i];
+                               }
+                       }
 
                        // Check if file exists, and confirm overwrite if necessary
                        Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
@@ -434,7 +492,14 @@ public class FileSaver
                                                                {
                                                                        try
                                                                        {
-                                                                               buffer.append(point.getTimestamp().getText());
+                                                                               if (timestampFormat == Timestamp.FORMAT_ORIGINAL) {
+                                                                                       // output original string
+                                                                                       buffer.append(point.getFieldValue(Field.TIMESTAMP));
+                                                                               }
+                                                                               else {
+                                                                                       // format value accordingly
+                                                                                       buffer.append(point.getTimestamp().getText(timestampFormat));
+                                                                               }
                                                                        }
                                                                        catch (NullPointerException npe) {}
                                                                }
diff --git a/tim/prune/save/GpxExporter.java b/tim/prune/save/GpxExporter.java
new file mode 100644 (file)
index 0000000..98619d1
--- /dev/null
@@ -0,0 +1,361 @@
+package tim.prune.save;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+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.JTextField;
+import javax.swing.filechooser.FileFilter;
+
+import tim.prune.GpsPruner;
+import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
+import tim.prune.data.Coordinate;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Timestamp;
+import tim.prune.data.Track;
+import tim.prune.data.TrackInfo;
+
+/**
+ * Class to export track information
+ * into a specified Gpx file
+ */
+public class GpxExporter implements Runnable
+{
+       private JFrame _parentFrame = null;
+       private Track _track = null;
+       private JDialog _dialog = null;
+       private JTextField _nameField = null;
+       private JTextField _descriptionField = null;
+       private JFileChooser _fileChooser = null;
+       private File _exportFile = null;
+
+       /** version number of Gpx */
+       private static final String GPX_VERSION_NUMBER = "1.1";
+       /** this program name */
+       private static final String GPX_CREATOR = "Prune v" + GpsPruner.VERSION_NUMBER + " activityworkshop.net";
+
+
+       /**
+        * Constructor giving frame and track
+        * @param inParentFrame parent frame
+        * @param inTrackInfo track info object to save
+        */
+       public GpxExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
+       {
+               _parentFrame = inParentFrame;
+               _track = inTrackInfo.getTrack();
+       }
+
+
+       /**
+        * Show the dialog to select options and export file
+        */
+       public void showDialog()
+       {
+               // Make dialog window
+               if (_dialog == null)
+               {
+                       _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportgpx.title"), true);
+                       _dialog.setLocationRelativeTo(_parentFrame);
+                       _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+                       _dialog.getContentPane().add(makeDialogComponents());
+                       _dialog.pack();
+               }
+               _dialog.show();
+       }
+
+
+       /**
+        * Create dialog components
+        * @return Panel containing all gui elements in dialog
+        */
+       private Component makeDialogComponents()
+       {
+               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 boxes
+               JPanel descPanel = new JPanel();
+               descPanel.setLayout(new GridLayout(2, 2));
+               descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
+               _nameField = new JTextField(10);
+               descPanel.add(_nameField);
+               descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
+               _descriptionField = new JTextField(10);
+               descPanel.add(_descriptionField);
+               mainPanel.add(descPanel);
+               dialogPanel.add(mainPanel, BorderLayout.CENTER);
+
+               // 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;
+       }
+
+
+       /**
+        * 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(".gpx")));
+                       }
+                       public String getDescription()
+                       {
+                               return I18nManager.getText("dialog.exportgpx.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();
+                               // Check file extension
+                               if (!file.getName().toLowerCase().endsWith(".gpx"))
+                               {
+                                       file = new File(file.getAbsolutePath() + ".gpx");
+                               }
+                               // 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)
+                               {
+                                       // New file or overwrite confirmed, so initiate export in separate thread
+                                       _exportFile = file;
+                                       new Thread(this).start();
+                               }
+                               else
+                               {
+                                       chooseAgain = true;
+                               }
+                       }
+               } while (chooseAgain);
+       }
+
+
+       /**
+        * Run method for controlling separate thread for exporting
+        */
+       public void run()
+       {
+               OutputStreamWriter writer = null;
+               try
+               {
+                       // normal writing to file
+                       writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
+                       // write file
+                       int numPoints = exportData(writer);
+
+                       // close file
+                       writer.close();
+                       JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
+                                + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2")
+                                + " " + _exportFile.getAbsolutePath(),
+                               I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
+                       // 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(),
+                               I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+               }
+               // 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
+        * @return number of points written
+        */
+       private int exportData(OutputStreamWriter inWriter) throws IOException
+       {
+               inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gpx version=\"");
+               inWriter.write(GPX_VERSION_NUMBER);
+               inWriter.write("\" creator=\"");
+               inWriter.write(GPX_CREATOR);
+               inWriter.write("\">\n");
+               // Name field
+               if (_nameField != null && _nameField.getText() != null && !_nameField.getText().equals(""))
+               {
+                       inWriter.write("\t<name>");
+                       inWriter.write(_nameField.getText());
+                       inWriter.write("</name>\n");
+               }
+               // Description field
+               inWriter.write("\t<desc>");
+               if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
+               {
+                       inWriter.write(_descriptionField.getText());
+               }
+               else
+               {
+                       inWriter.write("Export from Prune");
+               }
+               inWriter.write("</desc>\n");
+
+               int i = 0;
+               DataPoint point = null;
+               boolean hasTrackpoints = false;
+               // Loop over waypoints
+               int numPoints = _track.getNumPoints();
+               for (i=0; i<numPoints; i++)
+               {
+                       point = _track.getPoint(i);
+                       // Make a blob for each waypoint
+                       if (point.isWaypoint())
+                       {
+                               exportWaypoint(point, inWriter);
+                       }
+                       else
+                       {
+                               hasTrackpoints = true;
+                       }
+               }
+               // Make a line for the track, if there is one
+               // TODO: Look at segments of track, and split into separate track segments in Gpx if necessary
+               if (hasTrackpoints)
+               {
+                       inWriter.write("\t<trk><trkseg>\n");
+                       // Loop over track points
+                       for (i=0; i<numPoints; i++)
+                       {
+                               point = _track.getPoint(i);
+                               if (!point.isWaypoint())
+                               {
+                                       exportTrackpoint(point, inWriter);
+                               }
+                       }
+                       inWriter.write("\t</trkseg></trk>\n");
+               }
+               inWriter.write("</gpx>\n");
+               return numPoints;
+       }
+
+
+       /**
+        * Export the specified waypoint into the file
+        * @param inPoint waypoint to export
+        * @param inWriter writer object
+        * @throws IOException on write failure
+        */
+       private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException
+       {
+               inWriter.write("\t<wpt lat=\"");
+               inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+               inWriter.write("\" lon=\"");
+               inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+               inWriter.write("\">\n");
+               inWriter.write("\t\t<name>");
+               inWriter.write(inPoint.getWaypointName().trim());
+               inWriter.write("</name>\n");
+               // altitude if available
+               if (inPoint.hasAltitude())
+               {
+                       inWriter.write("\t\t<ele>");
+                       inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+                       inWriter.write("</ele>\n");
+               }
+               // timestamp if available (point might have altitude and then be turned into a waypoint)
+               if (inPoint.hasTimestamp())
+               {
+                       inWriter.write("\t\t<time>");
+                       inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
+                       inWriter.write("</time>\n");
+               }
+               // TODO: Include waypt type in Gpx
+               inWriter.write("\t</wpt>\n");
+       }
+
+
+       /**
+        * Export the specified trackpoint into the file
+        * @param inPoint trackpoint to export
+        * @param inWriter writer object
+        */
+       private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
+       {
+               inWriter.write("\t\t<trkpt lat=\"");
+               inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+               inWriter.write("\" lon=\"");
+               inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+               inWriter.write("\">");
+               // altitude
+               if (inPoint.hasAltitude())
+               {
+                       inWriter.write("<ele>");
+                       inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+                       inWriter.write("</ele>");
+               }
+               // timestamp if available
+               if (inPoint.hasTimestamp())
+               {
+                       inWriter.write("<time>");
+                       inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
+                       inWriter.write("</time>");
+               }
+               inWriter.write("</trkpt>\n");
+       }
+}
index 11474a6c8e8fb37f911f53f3f79e51edebc5fcf4..c091d0c74ac0f705be6acd1ebd977b6010a0af86 100644 (file)
@@ -35,8 +35,10 @@ import javax.swing.SwingConstants;
 import javax.swing.filechooser.FileFilter;
 
 import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
 import tim.prune.data.Coordinate;
 import tim.prune.data.DataPoint;
+import tim.prune.data.Field;
 import tim.prune.data.Track;
 import tim.prune.data.TrackInfo;
 import tim.prune.gui.ImageUtils;
@@ -52,6 +54,7 @@ public class KmlExporter implements Runnable
        private Track _track = null;
        private JDialog _dialog = null;
        private JTextField _descriptionField = null;
+       private JCheckBox _altitudesCheckbox = null;
        private JCheckBox _kmzCheckbox = null;
        private JCheckBox _exportImagesCheckbox = null;
        private JProgressBar _progressBar = null;
@@ -116,6 +119,10 @@ public class KmlExporter implements Runnable
                descPanel.add(_descriptionField);
                mainPanel.add(descPanel);
                dialogPanel.add(mainPanel, BorderLayout.CENTER);
+               // Checkbox for altitude export
+               _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
+               _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
+               mainPanel.add(_altitudesCheckbox);
                // Checkboxes for kmz export and image export
                _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
                _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
@@ -166,6 +173,8 @@ public class KmlExporter implements Runnable
         */
        private void enableCheckboxes()
        {
+               boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
+               if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
                boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
                _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
                _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
@@ -339,6 +348,7 @@ public class KmlExporter implements Runnable
                }
                inWriter.write("</name>\n");
 
+               boolean exportAltitudes = _altitudesCheckbox.isSelected();
                int i = 0;
                DataPoint point = null;
                boolean hasTrackpoints = false;
@@ -352,7 +362,11 @@ public class KmlExporter implements Runnable
                        // Make a blob for each waypoint
                        if (point.isWaypoint())
                        {
-                               exportWaypoint(point, inWriter);
+                               exportWaypoint(point, inWriter, exportAltitudes);
+                       }
+                       else if (point.getPhoto() == null)
+                       {
+                               hasTrackpoints = true;
                        }
                        // Make a blob with description for each photo
                        if (point.getPhoto() != null)
@@ -363,11 +377,7 @@ public class KmlExporter implements Runnable
                                        writtenPhotoHeader = true;
                                }
                                photoNum++;
-                               exportPhotoPoint(point, inWriter, inExportImages, photoNum);
-                       }
-                       else
-                       {
-                               hasTrackpoints = true;
+                               exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
                        }
                }
                // Make a line for the track, if there is one
@@ -375,14 +385,19 @@ public class KmlExporter implements Runnable
                {
                        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>");
+                               + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
+                               + "\t\t</Style>\n\t\t<LineString>\n");
+                       if (exportAltitudes) {
+                               inWriter.write("\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+                       }
+                       inWriter.write("\t\t\t<coordinates>");
                        // Loop over track points
                        for (i=0; i<numPoints; i++)
                        {
                                point = _track.getPoint(i);
-                               if (!point.isWaypoint())
+                               if (!point.isWaypoint() && point.getPhoto() == null)
                                {
-                                       exportTrackpoint(point, inWriter);
+                                       exportTrackpoint(point, inWriter, exportAltitudes);
                                }
                        }
                        inWriter.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
@@ -396,18 +411,30 @@ public class KmlExporter implements Runnable
         * Export the specified waypoint into the file
         * @param inPoint waypoint to export
         * @param inWriter writer object
+        * @param inExportAltitude true to include altitude
         * @throws IOException on write failure
         */
-       private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException
+       private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
        {
                inWriter.write("\t<Placemark>\n\t\t<name>");
                inWriter.write(inPoint.getWaypointName().trim());
                inWriter.write("</name>\n");
-               inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
+               inWriter.write("\t\t<Point>\n");
+               if (inExportAltitude && inPoint.hasAltitude()) {
+                       inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+               }
+               inWriter.write("\t\t\t<coordinates>");
                inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
                inWriter.write(',');
                inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
-               inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
+               inWriter.write(",");
+               if (inExportAltitude && inPoint.hasAltitude()) {
+                       inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+               }
+               else {
+                       inWriter.write("0");
+               }
+               inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
        }
 
 
@@ -417,9 +444,11 @@ public class KmlExporter implements Runnable
         * @param inWriter writer object
         * @param inImageLink flag to set whether to export image links or not
         * @param inImageNumber number of image for filename
+        * @param inExportAltitude true to include altitude
         * @throws IOException on write failure
         */
-       private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink, int inImageNumber)
+       private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
+               int inImageNumber, boolean inExportAltitude)
        throws IOException
        {
                inWriter.write("\t<Placemark>\n\t\t<name>");
@@ -436,11 +465,22 @@ public class KmlExporter implements Runnable
                                + "<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("\t\t<Point>\n");
+               if (inExportAltitude && inPoint.hasAltitude()) {
+                       inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+               }
+               inWriter.write("\t\t\t<coordinates>");
                inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
                inWriter.write(',');
                inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
-               inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
+               inWriter.write(",");
+               if (inExportAltitude && inPoint.hasAltitude()) {
+                       inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+               }
+               else {
+                       inWriter.write("0");
+               }
+               inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
        }
 
 
@@ -448,14 +488,22 @@ public class KmlExporter implements Runnable
         * Export the specified trackpoint into the file
         * @param inPoint trackpoint to export
         * @param inWriter writer object
+        * @param inExportAltitude true to include altitude
         */
-       private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
+       private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
        {
                inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
                inWriter.write(',');
                inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
-               // Altitude not exported, locked to ground by Google Earth
-               inWriter.write(",0\n");
+               // Altitude either absolute or locked to ground by Google Earth
+               inWriter.write(",");
+               if (inExportAltitude && inPoint.hasAltitude()) {
+                       inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+               }
+               else {
+                       inWriter.write("0");
+               }
+               inWriter.write("\n");
        }
 
 
index fd66590d1c3a6d6429993ac1f44f07a3b36ebba3..9b616f239f2d214b523133ed764e6f09ad288391 100644 (file)
@@ -15,6 +15,7 @@ public class PhotoTableModel extends AbstractTableModel
 
        /**
         * Constructor giving list size
+        * @param inSize number of photos
         */
        public PhotoTableModel(int inSize)
        {
@@ -75,7 +76,7 @@ public class PhotoTableModel extends AbstractTableModel
                {
                        return _photos[inRowIndex].getStatus();
                }
-               return new Boolean(_photos[inRowIndex].getSaveFlag());
+               return Boolean.valueOf(_photos[inRowIndex].getSaveFlag());
        }
 
 
index 3696dd10811986994943af9d7bdcc431a7f3eeb7..aacee115adecd4c95fdaa3b0ef0bcc6d0f803a04 100644 (file)
@@ -185,7 +185,7 @@ 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() {
index 274bfe98d59358c0ad0ef75e3fc14268625c9a69..ffdc1d662725ea77e3fc52a6bf9e86f8961168a8 100644 (file)
@@ -13,21 +13,27 @@ public class UpDownToggler implements ListSelectionListener
 {
        private JButton _upButton = null;
        private JButton _downButton = null;
-       private int _maxIndex = 0;
+       private int _maxIndex = 2;
 
        /**
-        * Constructor giving buttons and size
+        * Constructor giving buttons to enable/disable
         * @param inUpButton up button
         * @param inDownButton down button
-        * @param inListSize size of list
         */
-       public UpDownToggler(JButton inUpButton, JButton inDownButton, int inListSize)
+       public UpDownToggler(JButton inUpButton, JButton inDownButton)
        {
                _upButton = inUpButton;
                _downButton = inDownButton;
-               _maxIndex = inListSize - 1;
        }
 
+       /**
+        * Set the list size
+        * @param inListSize number of items in list
+        */
+       public void setListSize(int inListSize)
+       {
+               _maxIndex = inListSize - 1;
+       }
 
        /**
         * list selection has changed
index 52aeba36fe02ec7bee90345bc7d56e4b5b7f1cb1..df72bc758f9431bea0f5e069d97e514042ad1b5d 100644 (file)
@@ -17,6 +17,7 @@ public interface ThreeDWindow
 
        /**
         * Show the window
+        * @throws ThreeDException when 3d classes not found
         */
        public void show() throws ThreeDException;
 }
index 99e64327b7b8dd9aaf365fcc6bfe111bff6b0885..c5fc2f4ad702d9d2c79dc166dc0d0f18301719cf 100644 (file)
@@ -48,6 +48,8 @@ public class UndoConnectPhoto implements UndoOperation
                {\r
                        _point.setPhoto(null);\r
                        photo.setDataPoint(null);\r
+                       // inform subscribers\r
+                       inTrackInfo.triggerUpdate();\r
                }\r
                else\r
                {\r
diff --git a/tim/prune/undo/UndoCorrelatePhotos.java b/tim/prune/undo/UndoCorrelatePhotos.java
new file mode 100644 (file)
index 0000000..3c25837
--- /dev/null
@@ -0,0 +1,72 @@
+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 an auto-correlation of photos with points\r
+ */\r
+public class UndoCorrelatePhotos implements UndoOperation\r
+{\r
+       private DataPoint[] _contents = null;\r
+       private DataPoint[] _photoPoints = null;\r
+       private int _numPhotosCorrelated = -1;\r
+\r
+\r
+       /**\r
+        * Constructor\r
+        * @param inTrackInfo track information\r
+        */\r
+       public UndoCorrelatePhotos(TrackInfo inTrackInfo)\r
+       {\r
+               // Copy track contents\r
+               _contents = inTrackInfo.getTrack().cloneContents();\r
+               // Copy points associated with photos before correlation\r
+               int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();\r
+               _photoPoints = new DataPoint[numPhotos];\r
+               for (int i=0; i<numPhotos; i++) {\r
+                       _photoPoints[i] = inTrackInfo.getPhotoList().getPhoto(i).getDataPoint();\r
+               }\r
+       }\r
+\r
+       /**\r
+        * @param inNumCorrelated number of photos correlated\r
+        */\r
+       public void setNumPhotosCorrelated(int inNumCorrelated)\r
+       {\r
+               _numPhotosCorrelated = inNumCorrelated;\r
+       }\r
+\r
+       /**\r
+        * @return description of operation including parameters\r
+        */\r
+       public String getDescription()\r
+       {\r
+               return I18nManager.getText("undo.correlate") + " (" + _numPhotosCorrelated + ")";\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 track to previous values\r
+               inTrackInfo.getTrack().replaceContents(_contents);\r
+               // restore photo association\r
+               for (int i=0; i<_photoPoints.length; i++)\r
+               {\r
+                       Photo photo = inTrackInfo.getPhotoList().getPhoto(i);\r
+                       DataPoint point = _photoPoints[i];\r
+                       photo.setDataPoint(point);\r
+                       if (point != null) {\r
+                               point.setPhoto(photo);\r
+                       }\r
+               }\r
+               // clear selection\r
+               inTrackInfo.getSelection().clearAll();\r
+       }\r
+}
\ No newline at end of file
index 4e526cd8193d99b5c73154d3326ff2c2a0f6b904..04592aef43de183e7a2908eacf1ec17e9e2f552c 100644 (file)
@@ -17,8 +17,7 @@ public class UndoDeleteRange implements UndoOperation
 \r
        /**\r
         * Constructor\r
-        * @param inIndex index number of point within track\r
-        * @param inPoint data point\r
+        * @param inTrackInfo track info object\r
         */\r
        public UndoDeleteRange(TrackInfo inTrackInfo)\r
        {\r
diff --git a/tim/prune/undo/UndoDisconnectPhoto.java b/tim/prune/undo/UndoDisconnectPhoto.java
new file mode 100644 (file)
index 0000000..6cd27fd
--- /dev/null
@@ -0,0 +1,61 @@
+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 disconnection of a photo from a point\r
+ */\r
+public class UndoDisconnectPhoto implements UndoOperation\r
+{\r
+       private DataPoint _point = null;\r
+       private Photo _photo = 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 UndoDisconnectPhoto(DataPoint inPoint, String inFilename)\r
+       {\r
+               _point = inPoint;\r
+               _photo = inPoint.getPhoto();\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.disconnectphoto") + " " + _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
+               // Connect again\r
+               if (_point != null && _photo != null)\r
+               {\r
+                       _point.setPhoto(_photo);\r
+                       _photo.setDataPoint(_point);\r
+                       // inform subscribers\r
+                       inTrackInfo.triggerUpdate();\r
+               }\r
+               else\r
+               {\r
+                       // throw exception if failed\r
+                       throw new UndoException(getDescription());\r
+               }\r
+       }\r
+}
\ No newline at end of file
index d354b9dd8fb7a0e754d86fce62e12e30c1b4dc98..c9aee483868cac1d4f4c20b2e481331f112c18bf 100644 (file)
@@ -33,7 +33,7 @@ public class UndoLoad implements UndoOperation
 \r
        /**\r
         * Constructor for replacing\r
-        * @param inOldTrack track being replaced\r
+        * @param inOldTrackInfo track info being replaced\r
         * @param inNumLoaded number of points loaded\r
         * @param inPhotoList photo list, if any\r
         */\r
index 5efa1fe0b8790b2f92e5ef580c1f1b15b9179e84..20f26b9247e12550b985c5dc1e4f5f0ece7938a4 100644 (file)
@@ -16,6 +16,7 @@ public interface UndoOperation
        /**\r
         * Perform the undo operation on the specified track\r
         * @param inTrackInfo TrackInfo object on which to perform the operation\r
+        * @throws UndoException when undo fails\r
         */\r
        public void performUndo(TrackInfo inTrackInfo) throws UndoException;\r
 }
\ No newline at end of file