]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/App.java
Version 11, August 2010
[GpsPrune.git] / tim / prune / App.java
index 18a2e79a60fa737cde270045cb0d3b6263dc278f..88cafea3cd1901e23949ccba07579f3bdeabe8d4 100644 (file)
@@ -1,5 +1,7 @@
 package tim.prune;
 
+import java.io.File;
+import java.util.ArrayList;
 import java.util.EmptyStackException;
 import java.util.Set;
 import java.util.Stack;
@@ -7,45 +9,32 @@ 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.Altitude;
+import tim.prune.data.Checker;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
+import tim.prune.data.LatLonRectangle;
+import tim.prune.data.NumberUtils;
 import tim.prune.data.Photo;
 import tim.prune.data.PhotoList;
+import tim.prune.data.SourceInfo;
 import tim.prune.data.Track;
 import tim.prune.data.TrackInfo;
-import tim.prune.edit.FieldEditList;
-import tim.prune.edit.PointEditor;
-import tim.prune.edit.PointNameEditor;
+import tim.prune.function.SelectTracksFunction;
+import tim.prune.function.browser.BrowserLauncher;
+import tim.prune.function.browser.UrlGenerator;
+import tim.prune.function.edit.FieldEditList;
+import tim.prune.function.edit.PointEditor;
+import tim.prune.gui.SidebarController;
 import tim.prune.gui.MenuManager;
 import tim.prune.gui.UndoManager;
+import tim.prune.gui.Viewport;
 import tim.prune.load.FileLoader;
 import tim.prune.load.JpegLoader;
+import tim.prune.load.TrackNameList;
 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;
-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;
-import tim.prune.undo.UndoLoad;
-import tim.prune.undo.UndoLoadPhotos;
-import tim.prune.undo.UndoOperation;
-import tim.prune.undo.UndoRearrangeWaypoints;
-import tim.prune.undo.UndoReverseSection;
+import tim.prune.undo.*;
 
 
 /**
@@ -59,34 +48,28 @@ public class App
        private TrackInfo _trackInfo = null;
        private int _lastSavePosition = 0;
        private MenuManager _menuManager = null;
+       private SidebarController __sidebarController = null;
        private FileLoader _fileLoader = null;
        private JpegLoader _jpegLoader = 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;
-       private boolean _reversePointsConfirmed = false;
-
-       // Constants
-       public static final int REARRANGE_TO_START   = 0;
-       public static final int REARRANGE_TO_END     = 1;
-       public static final int REARRANGE_TO_NEAREST = 2;
+       private Stack<UndoOperation> _undoStack = null;
+       private boolean _mangleTimestampsConfirmed = false;
+       private Viewport _viewport = null;
+       private ArrayList<File> _dataFiles = null;
+       private boolean _firstDataFile = true;
 
 
        /**
         * Constructor
         * @param inFrame frame object for application
-        * @param inBroker message broker
         */
-       public App(JFrame inFrame, UpdateMessageBroker inBroker)
+       public App(JFrame inFrame)
        {
                _frame = inFrame;
-               _undoStack = new Stack();
-               _broker = inBroker;
-               _track = new Track(_broker);
-               _trackInfo = new TrackInfo(_track, _broker);
+               _undoStack = new Stack<UndoOperation>();
+               _track = new Track();
+               _trackInfo = new TrackInfo(_track);
+               FunctionLibrary.initialise(this);
        }
 
 
@@ -98,6 +81,14 @@ public class App
                return _trackInfo;
        }
 
+       /**
+        * @return the dialog frame
+        */
+       public JFrame getFrame()
+       {
+               return _frame;
+       }
+
        /**
         * Check if the application has unsaved data
         * @return true if data is unsaved, false otherwise
@@ -111,11 +102,43 @@ public class App
        /**
         * @return the undo stack
         */
-       public Stack getUndoStack()
+       public Stack<UndoOperation> getUndoStack()
        {
                return _undoStack;
        }
 
+       /**
+        * Load the specified data files one by one
+        * @param inDataFiles arraylist containing File objects to load
+        */
+       public void loadDataFiles(ArrayList<File> inDataFiles)
+       {
+               if (inDataFiles == null || inDataFiles.size() == 0) {
+                       _dataFiles = null;
+               }
+               else {
+                       _dataFiles = inDataFiles;
+                       File f = _dataFiles.get(0);
+                       _dataFiles.remove(0);
+                       // Start load of specified file
+                       if (_fileLoader == null)
+                               _fileLoader = new FileLoader(this, _frame);
+                       _firstDataFile = true;
+                       _fileLoader.openFile(f);
+               }
+       }
+
+       /**
+        * Complete a function execution
+        * @param inUndo undo object to be added to stack
+        * @param inConfirmText confirmation text
+        */
+       public void completeFunction(UndoOperation inUndo, String inConfirmText)
+       {
+               _undoStack.add(inUndo);
+               UpdateMessageBroker.informSubscribers(inConfirmText);
+       }
+
        /**
         * Set the MenuManager object to be informed about changes
         * @param inManager MenuManager object
@@ -138,131 +161,31 @@ public class App
 
 
        /**
-        * Add a photo or a directory of photos which are already correlated
+        * Add a photo or a directory of photos
         */
        public void addPhotos()
        {
                if (_jpegLoader == null)
                        _jpegLoader = new JpegLoader(this, _frame);
-               _jpegLoader.openFile();
+               _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
        }
 
-
        /**
         * Save the file in the selected format
         */
        public void saveFile()
        {
-               if (_track == null)
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
-                               I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+               if (_track == null) {
+                       showErrorMessage("error.save.dialogtitle", "error.save.nodata");
                }
                else
                {
                        if (_fileSaver == null) {
-                               _fileSaver = new FileSaver(this, _frame, _track);
-                       }
-                       _fileSaver.showDialog(_fileLoader.getLastUsedDelimiter());
-               }
-       }
-
-
-       /**
-        * Export track data as Kml
-        */
-       public void exportKml()
-       {
-               if (_track == null)
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
-                               I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
-               }
-               else
-               {
-                       // Invoke the export
-                       if (_kmlExporter == null)
-                       {
-                               _kmlExporter = new KmlExporter(_frame, _trackInfo);
-                       }
-                       _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);
+                               _fileSaver = new FileSaver(this, _frame);
                        }
-                       _gpxExporter.showDialog();
-               }
-       }
-
-
-       /**
-        * Export track data as Pov without specifying settings
-        */
-       public void exportPov()
-       {
-               exportPov(false, 0.0, 0.0, 0.0, 0);
-       }
-
-       /**
-        * Export track data as Pov and also specify settings
-        * @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
-        */
-       public void exportPov(double inX, double inY, double inZ, int inAltitudeCap)
-       {
-               exportPov(true, inX, inY, inZ, inAltitudeCap);
-       }
-
-       /**
-        * Export track data as Pov with optional angle specification
-        * @param inDefineAngles true to define angles, false to ignore
-        * @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)
-       {
-               // Check track has data to export
-               if (_track == null || _track.getNumPoints() <= 0)
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
-                               I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
-               }
-               else
-               {
-                       // Make new exporter if necessary
-                       if (_povExporter == null)
-                       {
-                               _povExporter = new PovExporter(_frame, _track);
-                       }
-                       // Specify angles if necessary
-                       if (inDefineSettings)
-                       {
-                               _povExporter.setCameraCoordinates(inX, inY, inZ);
-                               _povExporter.setAltitudeCap(inAltitudeCap);
-                       }
-                       // Initiate export
-                       _povExporter.showDialog();
+                       char delim = ',';
+                       if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
+                       _fileSaver.showDialog(delim);
                }
        }
 
@@ -319,27 +242,11 @@ public class App
                        // add information to undo stack
                        UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
                        // pass to track for completion
-                       if (_track.editPoint(currentPoint, inEditList))
+                       if (_track.editPoint(currentPoint, inEditList, false))
                        {
                                _undoStack.push(undo);
-                       }
-               }
-       }
-
-
-       /**
-        * Edit the name of the currently selected (way)point
-        */
-       public void editCurrentPointName()
-       {
-               if (_track != null)
-               {
-                       DataPoint currentPoint = _trackInfo.getCurrentPoint();
-                       if (currentPoint != null)
-                       {
-                               // Open point dialog to display details
-                               PointNameEditor editor = new PointNameEditor(this, _frame);
-                               editor.showDialog(currentPoint);
+                               // Confirm point edit
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
                        }
                }
        }
@@ -350,50 +257,53 @@ public class App
         */
        public void deleteCurrentPoint()
        {
-               if (_track != null)
+               if (_track == null) {return;}
+               DataPoint currentPoint = _trackInfo.getCurrentPoint();
+               if (currentPoint != null)
                {
-                       DataPoint currentPoint = _trackInfo.getCurrentPoint();
-                       if (currentPoint != null)
+                       boolean deletePhoto = false;
+                       Photo currentPhoto = currentPoint.getPhoto();
+                       if (currentPhoto != null)
+                       {
+                               // Confirm deletion of photo or decoupling
+                               int response = JOptionPane.showConfirmDialog(_frame,
+                                       I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
+                                       I18nManager.getText("dialog.deletepoint.title"),
+                                       JOptionPane.YES_NO_CANCEL_OPTION);
+                               if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
+                               {
+                                       // cancel pressed- abort delete
+                                       return;
+                               }
+                               if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
+                       }
+                       // store necessary information to undo it later
+                       int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
+                       int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
+                       DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
+                       // Construct Undo object
+                       UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
+                               nextTrackPoint != null && nextTrackPoint.getSegmentStart());
+                       // call track to delete point
+                       if (_trackInfo.deletePoint())
                        {
-                               boolean deletePhoto = false;
-                               Photo currentPhoto = currentPoint.getPhoto();
+                               // Delete was successful so add undo info to stack
+                               _undoStack.push(undo);
                                if (currentPhoto != null)
                                {
-                                       // Confirm deletion of photo or decoupling
-                                       int response = JOptionPane.showConfirmDialog(_frame,
-                                               I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
-                                               I18nManager.getText("dialog.deletepoint.title"),
-                                               JOptionPane.YES_NO_CANCEL_OPTION);
-                                       if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
+                                       // delete photo if necessary
+                                       if (deletePhoto)
                                        {
-                                               // cancel pressed- abort delete
-                                               return;
+                                               _trackInfo.getPhotoList().deletePhoto(photoIndex);
                                        }
-                                       if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
-                               }
-                               // add information to undo stack
-                               int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
-                               int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
-                               // Undo object needs to know index of photo in list (if any) to restore
-                               UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex);
-                               // call track to delete point
-                               if (_trackInfo.deletePoint())
-                               {
-                                       _undoStack.push(undo);
-                                       if (currentPhoto != null)
+                                       else
                                        {
-                                               // delete photo if necessary
-                                               if (deletePhoto)
-                                               {
-                                                       _trackInfo.getPhotoList().deletePhoto(photoIndex);
-                                               }
-                                               else
-                                               {
-                                                       // decouple photo from point
-                                                       currentPhoto.setDataPoint(null);
-                                               }
+                                               // decouple photo from point
+                                               currentPhoto.setDataPoint(null);
                                        }
                                }
+                               // Confirm
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
                        }
                }
        }
@@ -453,7 +363,7 @@ public class App
                                        }
                                }
                                // add information to undo stack
-                               UndoOperation undo = new UndoDeleteRange(_trackInfo);
+                               UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
                                // delete requested photos
                                for (int i=0; i<numToDelete; i++)
                                {
@@ -476,6 +386,9 @@ public class App
                                if (_trackInfo.deleteRange())
                                {
                                        _undoStack.push(undo);
+                                       // Confirm
+                                       UpdateMessageBroker.informSubscribers("" + numToDelete + " "
+                                               + I18nManager.getText("confirm.deletepoint.multi"));
                                }
                        }
                }
@@ -483,77 +396,28 @@ public class App
 
 
        /**
-        * Delete all the duplicate points in the track
-        */
-       public void deleteDuplicates()
-       {
-               if (_track != null)
-               {
-                       // Save undo information
-                       UndoOperation undo = new UndoDeleteDuplicates(_track);
-                       // tell track to do it
-                       int numDeleted = _trackInfo.deleteDuplicates();
-                       if (numDeleted > 0)
-                       {
-                               _undoStack.add(undo);
-                               String message = null;
-                               if (numDeleted == 1)
-                               {
-                                       message = "1 " + I18nManager.getText("dialog.deleteduplicates.single.text");
-                               }
-                               else
-                               {
-                                       message = "" + numDeleted + " " + I18nManager.getText("dialog.deleteduplicates.multi.text");
-                               }
-                               JOptionPane.showMessageDialog(_frame, message,
-                                       I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE);
-                       }
-                       else
-                       {
-                               JOptionPane.showMessageDialog(_frame,
-                                       I18nManager.getText("dialog.deleteduplicates.nonefound"),
-                                       I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE);
-                       }
-               }
-       }
-
-
-       /**
-        * Compress the track
+        * Finish the compression by deleting the marked points
         */
-       public void compressTrack()
+       public void finishCompressTrack()
        {
                UndoCompress undo = new UndoCompress(_track);
-               // Get compression parameter
-               Object compParam = JOptionPane.showInputDialog(_frame,
-                       I18nManager.getText("dialog.compresstrack.parameter.text"),
-                       I18nManager.getText("dialog.compresstrack.title"),
-                       JOptionPane.QUESTION_MESSAGE, null, null, "100");
-               int compNumber = parseNumber(compParam);
-               if (compNumber <= 0) return;
                // call track to do compress
-               int numPointsDeleted = _trackInfo.compress(compNumber);
+               int numPointsDeleted = _trackInfo.deleteMarkedPoints();
                // add to undo stack if successful
                if (numPointsDeleted > 0)
                {
                        undo.setNumPointsDeleted(numPointsDeleted);
                        _undoStack.add(undo);
-                       JOptionPane.showMessageDialog(_frame,
-                               I18nManager.getText("dialog.compresstrack.text") + " - "
-                                + numPointsDeleted + " "
-                                + (numPointsDeleted==1?I18nManager.getText("dialog.compresstrack.single.text"):I18nManager.getText("dialog.compresstrack.multi.text")),
-                               I18nManager.getText("dialog.compresstrack.title"), JOptionPane.INFORMATION_MESSAGE);
+                       UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
+                                + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
                }
-               else
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.compresstrack.nonefound"),
-                               I18nManager.getText("dialog.compresstrack.title"), JOptionPane.WARNING_MESSAGE);
+               else {
+                       showErrorMessage("function.compress", "dialog.compress.nonefound");
                }
        }
 
-
        /**
-        * Reverse a section of the track
+        * Reverse the currently selected section of the track
         */
        public void reverseRange()
        {
@@ -561,17 +425,94 @@ public class App
                int selStart = _trackInfo.getSelection().getStart();
                int selEnd = _trackInfo.getSelection().getEnd();
                if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
-                       || _reversePointsConfirmed
+                       || _mangleTimestampsConfirmed
                        || (JOptionPane.showConfirmDialog(_frame,
                                 I18nManager.getText("dialog.confirmreversetrack.text"),
                                 I18nManager.getText("dialog.confirmreversetrack.title"),
-                                JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_reversePointsConfirmed = true)))
+                                JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
                {
-                       UndoReverseSection undo = new UndoReverseSection(selStart, selEnd);
+                       UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
                        // call track to reverse range
                        if (_track.reverseRange(selStart, selEnd))
                        {
                                _undoStack.add(undo);
+                               // Confirm
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
+                       }
+               }
+       }
+
+       /**
+        * Complete the add time offset function with the specified offset
+        * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
+        */
+       public void finishAddTimeOffset(long inTimeOffset)
+       {
+               // Construct undo information
+               int selStart = _trackInfo.getSelection().getStart();
+               int selEnd = _trackInfo.getSelection().getEnd();
+               UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
+               if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
+               {
+                       _undoStack.add(undo);
+                       UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
+                       UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
+               }
+       }
+
+
+       /**
+        * Complete the add altitude offset function with the specified offset
+        * @param inOffset altitude offset to add as String
+        * @param inFormat altitude format of offset (eg Feet, Metres)
+        */
+       public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
+       {
+               // Sanity check
+               if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
+                       return;
+               }
+               // Construct undo information
+               UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
+               int selStart = _trackInfo.getSelection().getStart();
+               int selEnd = _trackInfo.getSelection().getEnd();
+               // How many decimal places are given in the offset?
+               int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
+               boolean success = false;
+               // Decimal offset given
+               try {
+                       double offsetd = Double.parseDouble(inOffset);
+                       success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
+               }
+               catch (NumberFormatException nfe) {}
+               if (success)
+               {
+                       _undoStack.add(undo);
+                       _trackInfo.getSelection().markInvalid();
+                       UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
+                       UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
+               }
+       }
+
+
+       /**
+        * Merge the track segments within the current selection
+        */
+       public void mergeTrackSegments()
+       {
+               if (_trackInfo.getSelection().hasRangeSelected())
+               {
+                       // Maybe could check segment start flags to see if it's worth merging
+                       // If first track point is already start and no other seg starts then do nothing
+
+                       int selStart = _trackInfo.getSelection().getStart();
+                       int selEnd = _trackInfo.getSelection().getEnd();
+                       // Make undo object
+                       UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
+                       // Call track to merge segments
+                       if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
+                               _undoStack.add(undo);
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
                        }
                }
        }
@@ -601,69 +542,80 @@ public class App
 
 
        /**
-        * Rearrange the waypoints into track order
-        * @param inFunction nearest point, all to end or all to start
+        * Average the selected points
         */
-       public void rearrangeWaypoints(int inFunction)
+       public void averageSelection()
        {
-               UndoRearrangeWaypoints undo = new UndoRearrangeWaypoints(_track);
-               boolean success = false;
-               if (inFunction == REARRANGE_TO_START || inFunction == REARRANGE_TO_END)
-               {
-                       // Collect the waypoints to the start or end of the track
-                       success = _track.collectWaypoints(inFunction == REARRANGE_TO_START);
-               }
-               else
-               {
-                       // Interleave the waypoints into track order
-                       success = _track.interleaveWaypoints();
-               }
-               if (success)
+               // Find following track point
+               DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
+               boolean segFlag = false;
+               if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
+               UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
+               // call track info object to do the averaging
+               if (_trackInfo.average())
                {
                        _undoStack.add(undo);
                }
-               else
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.rearrange.noop"),
-                               I18nManager.getText("error.function.noop.title"), JOptionPane.WARNING_MESSAGE);
-               }
        }
 
 
        /**
-        * Open a new window with the 3d view
+        * Create a new point at the given position
+        * @param inPoint point to add
         */
-       public void show3dWindow()
+       public void createPoint(DataPoint inPoint)
        {
-               ThreeDWindow window = WindowFactory.getWindow(this, _frame);
-               if (window == null)
-               {
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.function.nojava3d"),
-                               I18nManager.getText("error.function.notavailable.title"), JOptionPane.WARNING_MESSAGE);
-               }
-               else
-               {
-                       try
-                       {
-                               // Pass the track object and show the window
-                               window.setTrack(_track);
-                               window.show();
-                       }
-                       catch (ThreeDException e)
-                       {
-                               JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.3d") + ": " + e.getMessage(),
-                                       I18nManager.getText("error.3d.title"), JOptionPane.ERROR_MESSAGE);
-                       }
-               }
+               // create undo object
+               UndoCreatePoint undo = new UndoCreatePoint();
+               _undoStack.add(undo);
+               // add point to track
+               inPoint.setSegmentStart(true);
+               _track.appendPoints(new DataPoint[] {inPoint});
+               // ensure track's field list contains point's fields
+               _track.extendFieldList(inPoint.getFieldList());
+               _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
+               // update listeners
+               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
        }
 
 
        /**
-        * Select all points
+        * Cut the current selection and move it to before the currently selected point
         */
-       public void selectAll()
+       public void cutAndMoveSelection()
        {
-               _trackInfo.getSelection().select(0, 0, _track.getNumPoints()-1);
+               int startIndex = _trackInfo.getSelection().getStart();
+               int endIndex = _trackInfo.getSelection().getEnd();
+               int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
+               // If timestamps would be mangled by cut/move, confirm
+               if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
+                       || _mangleTimestampsConfirmed
+                       || (JOptionPane.showConfirmDialog(_frame,
+                                I18nManager.getText("dialog.confirmcutandmove.text"),
+                                I18nManager.getText("dialog.confirmcutandmove.title"),
+                                JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
+               {
+                       // Find points to set segment flags
+                       DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
+                       DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
+                       DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
+                       // Make undo object
+                       UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
+                       // Call track info to move track section
+                       if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
+                       {
+                               // Set segment start flags (first track point, next track point, move to point)
+                               if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
+                               if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
+                               if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
+
+                               // Add undo object to stack, set confirm message
+                               _undoStack.add(undo);
+                               _trackInfo.getSelection().selectRange(-1, -1);
+                               UpdateMessageBroker.informSubscribers();
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
+                       }
+               }
        }
 
        /**
@@ -673,83 +625,158 @@ public class App
        {
                // deselect point, range and photo
                _trackInfo.getSelection().clearAll();
+               _track.clearDeletionMarkers();
        }
 
+       /**
+        * Receive loaded data and start load
+        * @param inFieldArray array of fields
+        * @param inDataArray array of data
+        * @param inAltFormat altitude format
+        * @param inSourceInfo information about the source of the data
+        */
+       public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
+               Altitude.Format inAltFormat, SourceInfo inSourceInfo)
+       {
+               informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo, null);
+       }
 
        /**
-        * Receive loaded data and optionally merge with current Track
+        * Receive loaded data and determine whether to filter on tracks or not
         * @param inFieldArray array of fields
         * @param inDataArray array of data
         * @param inAltFormat altitude format
-        * @param inFilename filename used
+        * @param inSourceInfo information about the source of the data
+        * @param inTrackNameList information about the track names
         */
-       public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
+       public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
+               Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
        {
                // Check whether loaded array can be properly parsed into a Track
-               Track loadedTrack = new Track(_broker);
+               Track loadedTrack = new Track();
                loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
                if (loadedTrack.getNumPoints() <= 0)
                {
-                       JOptionPane.showMessageDialog(_frame,
-                               I18nManager.getText("error.load.nopoints"),
-                               I18nManager.getText("error.load.dialogtitle"),
-                               JOptionPane.ERROR_MESSAGE);
+                       showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
+                       // load next file if there's a queue
+                       loadNextFile();
                        return;
                }
+               // Check for doubled track
+               if (Checker.isDoubledTrack(loadedTrack)) {
+                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
+                               I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
+               }
+               // Look at TrackNameList, decide whether to filter or not
+               if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
+               {
+                       // Launch a dialog to let the user choose which tracks to load, then continue
+                       new SelectTracksFunction(this, inFieldArray, inDataArray, inAltFormat, inSourceInfo,
+                               inTrackNameList).begin();
+               }
+               else {
+                       // go directly to load
+                       informDataLoaded(loadedTrack, inSourceInfo);
+               }
+       }
+
+       /**
+        * Receive loaded data and optionally merge with current Track
+        * @param inLoadedTrack loaded track
+        * @param inSourceInfo information about the source of the data
+        */
+       public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
+       {
                // Decide whether to load or append
-               if (_track != null && _track.getNumPoints() > 0)
+               if (_track.getNumPoints() > 0)
                {
                        // ask whether to replace or append
-                       int answer = JOptionPane.showConfirmDialog(_frame,
-                               I18nManager.getText("dialog.openappend.text"),
-                               I18nManager.getText("dialog.openappend.title"),
-                               JOptionPane.YES_NO_CANCEL_OPTION);
+                       int answer = 0;
+                       if (_dataFiles == null || _firstDataFile) {
+                               answer = JOptionPane.showConfirmDialog(_frame,
+                                       I18nManager.getText("dialog.openappend.text"),
+                                       I18nManager.getText("dialog.openappend.title"),
+                                       JOptionPane.YES_NO_CANCEL_OPTION);
+                       }
+                       else {
+                               // Automatically append if there's a file load queue
+                               answer = JOptionPane.YES_OPTION;
+                       }
                        if (answer == JOptionPane.YES_OPTION)
                        {
                                // append data to current Track
-                               _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
-                               _track.combine(loadedTrack);
-                               // set filename if currently empty
-                               if (_trackInfo.getFileInfo().getNumFiles() == 0)
-                               {
-                                       _trackInfo.getFileInfo().setFile(inFilename);
-                               }
-                               else
-                               {
-                                       _trackInfo.getFileInfo().addFile();
-                               }
+                               _undoStack.add(new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints()));
+                               _track.combine(inLoadedTrack);
+                               // set source information
+                               inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
+                               _trackInfo.getFileInfo().addSource(inSourceInfo);
                        }
                        else if (answer == JOptionPane.NO_OPTION)
                        {
                                // Don't append, replace data
                                PhotoList photos = null;
-                               if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
-                               {
+                               if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
                                        photos = _trackInfo.getPhotoList().cloneList();
                                }
-                               _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
+                               _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), 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)
-                               {
+                               _trackInfo.getSelection().clearAll();
+                               _track.load(inLoadedTrack);
+                               inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
+                               _trackInfo.getFileInfo().replaceSource(inSourceInfo);
+                               if (photos != null) {
                                        _trackInfo.getPhotoList().removeCorrelatedPhotos();
                                }
                        }
                }
                else
                {
-                       // currently no data held, so use received data
-                       _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
+                       // Currently no data held, so transfer received data
+                       _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null));
                        _lastSavePosition = _undoStack.size();
-                       _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
-                       _trackInfo.getFileInfo().setFile(inFilename);
+                       _trackInfo.getSelection().clearAll();
+                       _track.load(inLoadedTrack);
+                       inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
+                       _trackInfo.getFileInfo().addSource(inSourceInfo);
                }
-               _broker.informSubscribers();
+               UpdateMessageBroker.informSubscribers();
+               // Update status bar
+               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
+                       + " '" + inSourceInfo.getName() + "'");
                // update menu
                _menuManager.informFileLoaded();
+               // load next file if there's a queue
+               loadNextFile();
+       }
+
+       /**
+        * Inform the app that NO data was loaded, eg cancel pressed
+        * Only needed if there's another file waiting in the queue
+        */
+       public void informNoDataLoaded()
+       {
+               // Load next file if there's a queue
+               loadNextFile();
+       }
+
+       /**
+        * Load the next file in the waiting list, if any
+        */
+       private void loadNextFile()
+       {
+               _firstDataFile = false;
+               if (_dataFiles == null || _dataFiles.size() == 0) {
+                       _dataFiles = null;
+               }
+               else {
+                       new Thread(new Runnable() {
+                               public void run() {
+                                       File f = _dataFiles.get(0);
+                                       _dataFiles.remove(0);
+                                       _fileLoader.openFile(f);
+                               }
+                       }).start();
+               }
        }
 
 
@@ -757,7 +784,7 @@ public class App
         * Accept a list of loaded photos
         * @param inPhotoSet Set of Photo objects
         */
-       public void informPhotosLoaded(Set inPhotoSet)
+       public void informPhotosLoaded(Set<Photo> inPhotoSet)
        {
                if (inPhotoSet != null && !inPhotoSet.isEmpty())
                {
@@ -769,20 +796,14 @@ public class App
                                // Save numbers so load can be undone
                                _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
                        }
-                       if (numPhotosAdded == 1)
-                       {
-                               JOptionPane.showMessageDialog(_frame,
-                                       "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photoadded"),
-                                       I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE);
+                       if (numPhotosAdded == 1) {
+                               UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
                        }
-                       else
-                       {
-                               JOptionPane.showMessageDialog(_frame,
-                                       "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photosadded"),
-                                       I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE);
+                       else {
+                               UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
                        }
-                       // TODO: Improve message when photo(s) fail to load (eg already added)
-                       _broker.informSubscribers();
+                       // MAYBE: Improve message when photo(s) fail to load (eg already added)
+                       UpdateMessageBroker.informSubscribers();
                        // update menu
                        _menuManager.informFileLoaded();
                }
@@ -796,13 +817,17 @@ public class App
        {
                Photo photo = _trackInfo.getCurrentPhoto();
                DataPoint point = _trackInfo.getCurrentPoint();
-               if (photo != null && point != null && point.getPhoto() == null)
+               if (photo != null && point != null)
                {
-                       // connect
-                       _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
-                       photo.setDataPoint(point);
-                       point.setPhoto(photo);
-                       _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+                       if (point.getPhoto() == null)
+                       {
+                               // point doesn't currently have a photo, so just connect it
+                               _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
+                               photo.setDataPoint(point);
+                               point.setPhoto(photo);
+                               UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
+                       }
                }
        }
 
@@ -820,7 +845,8 @@ public class App
                        // disconnect
                        photo.setDataPoint(null);
                        point.setPhoto(null);
-                       _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+                       UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+                       UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
                }
        }
 
@@ -869,117 +895,6 @@ 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
         */
@@ -1006,6 +921,7 @@ public class App
        {
                if (_undoStack.isEmpty())
                {
+                       // Nothing to undo
                        JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
                                I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
                }
@@ -1036,7 +952,7 @@ public class App
                        _undoStack.clear();
                        _lastSavePosition = 0;
                        if (unsaved) _lastSavePosition = -1;
-                       _broker.informSubscribers();
+                       UpdateMessageBroker.informSubscribers();
                }
        }
 
@@ -1051,21 +967,18 @@ public class App
                {
                        for (int i=0; i<inNumUndos; i++)
                        {
-                               ((UndoOperation) _undoStack.pop()).performUndo(_trackInfo);
+                               _undoStack.pop().performUndo(_trackInfo);
                        }
-                       JOptionPane.showMessageDialog(_frame, "" + inNumUndos + " "
-                                + (inNumUndos==1?I18nManager.getText("dialog.confirmundo.single.text"):I18nManager.getText("dialog.confirmundo.multiple.text")),
-                               I18nManager.getText("dialog.confirmundo.title"),
-                               JOptionPane.INFORMATION_MESSAGE);
+                       String message = "" + inNumUndos + " "
+                                + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
+                       UpdateMessageBroker.informSubscribers(message);
                }
                catch (UndoException ue)
                {
-                       JOptionPane.showMessageDialog(_frame,
-                               I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage(),
-                               I18nManager.getText("error.undofailed.title"),
-                               JOptionPane.ERROR_MESSAGE);
+                       showErrorMessageNoLookup("error.undofailed.title",
+                               I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
                        _undoStack.clear();
-                       _broker.informSubscribers();
+                       UpdateMessageBroker.informSubscribers();
                }
                catch (EmptyStackException empty) {}
        }
@@ -1092,12 +1005,66 @@ public class App
        }
 
        /**
-        * Show a brief help message
+        * Show a map url in an external browser
+        * @param inSourceIndex index of map source to use
+        */
+       public void showExternalMap(int inSourceIndex)
+       {
+               BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
+       }
+
+       /**
+        * Display a standard error message
+        * @param inTitleKey key to lookup for window title
+        * @param inMessageKey key to lookup for error message
+        */
+       public void showErrorMessage(String inTitleKey, String inMessageKey)
+       {
+               JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
+                       I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
+       }
+
+       /**
+        * Display a standard error message
+        * @param inTitleKey key to lookup for window title
+        * @param inMessage error message
+        */
+       public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
+       {
+               JOptionPane.showMessageDialog(_frame, inMessage,
+                       I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
+       }
+
+       /**
+        * @param inViewport viewport object
+        */
+       public void setViewport(Viewport inViewport)
+       {
+               _viewport = inViewport;
+       }
+
+       /**
+        * @return current viewport object
+        */
+       public Viewport getViewport()
+       {
+               return _viewport;
+       }
+
+       /**
+        * Set the controller for the full screen mode
+        * @param inController controller object
+        */
+       public void setSidebarController(SidebarController inController)
+       {
+               __sidebarController = inController;
+       }
+
+       /**
+        * Toggle sidebars on and off
         */
-       public void showHelp()
+       public void toggleSidebars()
        {
-               JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.help.help"),
-                       I18nManager.getText("menu.help"),
-                       JOptionPane.INFORMATION_MESSAGE);
+               __sidebarController.toggle();
        }
 }