]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/App.java
Version 19, May 2018
[GpsPrune.git] / tim / prune / App.java
index 88cafea3cd1901e23949ccba07579f3bdeabe8d4..3a778588ab815a7f30c4b396e4de9d2c266836da 100644 (file)
@@ -4,12 +4,11 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.EmptyStackException;
 import java.util.Set;
-import java.util.Stack;
 
 import javax.swing.JFrame;
 import javax.swing.JOptionPane;
 
-import tim.prune.data.Altitude;
+import tim.prune.config.Config;
 import tim.prune.data.Checker;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
@@ -17,23 +16,31 @@ import tim.prune.data.LatLonRectangle;
 import tim.prune.data.NumberUtils;
 import tim.prune.data.Photo;
 import tim.prune.data.PhotoList;
+import tim.prune.data.PointCreateOptions;
+import tim.prune.data.RecentFile;
 import tim.prune.data.SourceInfo;
 import tim.prune.data.Track;
 import tim.prune.data.TrackInfo;
+import tim.prune.data.SourceInfo.FILE_TYPE;
+import tim.prune.data.Unit;
+import tim.prune.function.AsyncMediaLoader;
 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.function.settings.SaveConfig;
 import tim.prune.gui.MenuManager;
+import tim.prune.gui.SidebarController;
 import tim.prune.gui.UndoManager;
 import tim.prune.gui.Viewport;
+import tim.prune.gui.colour.ColourerCaretaker;
+import tim.prune.gui.colour.PointColourer;
 import tim.prune.load.FileLoader;
 import tim.prune.load.JpegLoader;
+import tim.prune.load.MediaLinkInfo;
 import tim.prune.load.TrackNameList;
 import tim.prune.save.ExifSaver;
 import tim.prune.save.FileSaver;
+import tim.prune.tips.TipManager;
 import tim.prune.undo.*;
 
 
@@ -48,15 +55,21 @@ public class App
        private TrackInfo _trackInfo = null;
        private int _lastSavePosition = 0;
        private MenuManager _menuManager = null;
-       private SidebarController __sidebarController = null;
+       private SidebarController _sidebarController = null;
        private FileLoader _fileLoader = null;
        private JpegLoader _jpegLoader = null;
        private FileSaver _fileSaver = null;
-       private Stack<UndoOperation> _undoStack = null;
+       private UndoStack _undoStack = null;
+       private ColourerCaretaker _colCaretaker = null;
        private boolean _mangleTimestampsConfirmed = false;
        private Viewport _viewport = null;
        private ArrayList<File> _dataFiles = null;
-       private boolean _firstDataFile = true;
+       private boolean _autoAppendNextFile = false;
+       private boolean _busyLoading = false;
+       private AppMode _appMode = AppMode.NORMAL;
+
+       /** Enum for the app mode - currently only two options but may expand later */
+       public enum AppMode {NORMAL, DRAWRECT};
 
 
        /**
@@ -66,10 +79,13 @@ public class App
        public App(JFrame inFrame)
        {
                _frame = inFrame;
-               _undoStack = new Stack<UndoOperation>();
+               _undoStack = new UndoStack();
                _track = new Track();
                _trackInfo = new TrackInfo(_track);
                FunctionLibrary.initialise(this);
+               _colCaretaker = new ColourerCaretaker(this);
+               UpdateMessageBroker.addSubscriber(_colCaretaker);
+               _colCaretaker.setColourer(Config.getPointColourer());
        }
 
 
@@ -96,17 +112,51 @@ public class App
        public boolean hasDataUnsaved()
        {
                return (_undoStack.size() > _lastSavePosition
-                       && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
+                       && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
        }
 
        /**
         * @return the undo stack
         */
-       public Stack<UndoOperation> getUndoStack()
+       public UndoStack getUndoStack()
        {
                return _undoStack;
        }
 
+       /**
+        * Update the system's point colourer using the one in the Config
+        */
+       public void updatePointColourer()
+       {
+               if (_colCaretaker != null) {
+                       _colCaretaker.setColourer(Config.getPointColourer());
+               }
+       }
+
+       /**
+        * @return colourer object, or null
+        */
+       public PointColourer getPointColourer()
+       {
+               if (_colCaretaker == null) {return null;}
+               return _colCaretaker.getColourer();
+       }
+
+       /**
+        * Show the specified tip if appropriate
+        * @param inTipNumber tip number from TipManager
+        */
+       public void showTip(int inTipNumber)
+       {
+               String key = TipManager.fireTipTrigger(inTipNumber);
+               if (key != null && !key.equals(""))
+               {
+                       JOptionPane.showMessageDialog(_frame, I18nManager.getText(key),
+                               I18nManager.getText("tip.title"), JOptionPane.INFORMATION_MESSAGE);
+               }
+       }
+
+
        /**
         * Load the specified data files one by one
         * @param inDataFiles arraylist containing File objects to load
@@ -116,14 +166,15 @@ public class App
                if (inDataFiles == null || inDataFiles.size() == 0) {
                        _dataFiles = null;
                }
-               else {
+               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;
+                       _autoAppendNextFile = false; // prompt for append
                        _fileLoader.openFile(f);
                }
        }
@@ -137,6 +188,7 @@ public class App
        {
                _undoStack.add(inUndo);
                UpdateMessageBroker.informSubscribers(inConfirmText);
+               setCurrentMode(AppMode.NORMAL);
        }
 
        /**
@@ -206,6 +258,10 @@ public class App
                                JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
                        == JOptionPane.YES_OPTION)
                {
+                       // save settings
+                       if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
+                               new SaveConfig(this).silentSave();
+                       }
                        System.exit(0);
                }
        }
@@ -244,7 +300,7 @@ public class App
                        // pass to track for completion
                        if (_track.editPoint(currentPoint, inEditList, false))
                        {
-                               _undoStack.push(undo);
+                               _undoStack.add(undo);
                                // Confirm point edit
                                UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
                        }
@@ -261,13 +317,14 @@ public class App
                DataPoint currentPoint = _trackInfo.getCurrentPoint();
                if (currentPoint != null)
                {
+                       // Check for photo
                        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.deletephoto") + " " + currentPhoto.getName(),
                                        I18nManager.getText("dialog.deletepoint.title"),
                                        JOptionPane.YES_NO_CANCEL_OPTION);
                                if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
@@ -280,15 +337,18 @@ public class App
                        // store necessary information to undo it later
                        int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
                        int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
+                       int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
                        DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
                        // Construct Undo object
-                       UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
-                               nextTrackPoint != null && nextTrackPoint.getSegmentStart());
+                       UndoDeletePoint undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
+                               audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
+                       undo.setAtBoundaryOfSelectedRange(pointIndex == _trackInfo.getSelection().getStart() ||
+                               pointIndex == _trackInfo.getSelection().getEnd());
                        // call track to delete point
                        if (_trackInfo.deletePoint())
                        {
                                // Delete was successful so add undo info to stack
-                               _undoStack.push(undo);
+                               _undoStack.add(undo);
                                if (currentPhoto != null)
                                {
                                        // delete photo if necessary
@@ -301,121 +361,20 @@ public class App
                                                // decouple photo from point
                                                currentPhoto.setDataPoint(null);
                                        }
+                                       UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
+                               }
+                               // Delete audio object (without bothering to ask)
+                               if (audioIndex > -1) {
+                                       _trackInfo.getAudioList().deleteAudio(audioIndex);
                                }
                                // Confirm
                                UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
+                               UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
                        }
                }
        }
 
 
-       /**
-        * Delete the currently selected range
-        */
-       public void deleteSelectedRange()
-       {
-               if (_track != null)
-               {
-                       // Find out if photos should be deleted or not
-                       int selStart = _trackInfo.getSelection().getStart();
-                       int selEnd = _trackInfo.getSelection().getEnd();
-                       if (selStart >= 0 && selEnd >= selStart)
-                       {
-                               int numToDelete = selEnd - selStart + 1;
-                               boolean[] deletePhotos = new boolean[numToDelete];
-                               Photo[] photosToDelete = new Photo[numToDelete];
-                               boolean deleteAll = false;
-                               boolean deleteNone = false;
-                               String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
-                                       I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
-                                       I18nManager.getText("button.cancel")};
-                               DataPoint point = null;
-                               for (int i=0; i<numToDelete; i++)
-                               {
-                                       point = _trackInfo.getTrack().getPoint(i + selStart);
-                                       if (point != null && point.getPhoto() != null)
-                                       {
-                                               if (deleteAll)
-                                               {
-                                                       deletePhotos[i] = true;
-                                                       photosToDelete[i] = point.getPhoto();
-                                               }
-                                               else if (deleteNone) {deletePhotos[i] = false;}
-                                               else
-                                               {
-                                                       int response = JOptionPane.showOptionDialog(_frame,
-                                                               I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
-                                                               I18nManager.getText("dialog.deletepoint.title"),
-                                                               JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
-                                                               questionOptions, questionOptions[1]);
-                                                       // check for cancel or close
-                                                       if (response == 4 || response == -1) {return;}
-                                                       // check for yes or yes to all
-                                                       if (response == 0 || response == 2)
-                                                       {
-                                                               deletePhotos[i] = true;
-                                                               photosToDelete[i] = point.getPhoto();
-                                                               if (response == 2) {deleteAll = true;}
-                                                       }
-                                                       // check for no to all
-                                                       if (response == 3) {deleteNone = true;}
-                                               }
-                                       }
-                               }
-                               // add information to undo stack
-                               UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
-                               // delete requested photos
-                               for (int i=0; i<numToDelete; i++)
-                               {
-                                       point = _trackInfo.getTrack().getPoint(i + selStart);
-                                       if (point != null && point.getPhoto() != null)
-                                       {
-                                               if (deletePhotos[i])
-                                               {
-                                                       // delete photo from list
-                                                       _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
-                                               }
-                                               else
-                                               {
-                                                       // decouple from point
-                                                       point.getPhoto().setDataPoint(null);
-                                               }
-                                       }
-                               }
-                               // call track to delete range
-                               if (_trackInfo.deleteRange())
-                               {
-                                       _undoStack.push(undo);
-                                       // Confirm
-                                       UpdateMessageBroker.informSubscribers("" + numToDelete + " "
-                                               + I18nManager.getText("confirm.deletepoint.multi"));
-                               }
-                       }
-               }
-       }
-
-
-       /**
-        * Finish the compression by deleting the marked points
-        */
-       public void finishCompressTrack()
-       {
-               UndoCompress undo = new UndoCompress(_track);
-               // call track to do compress
-               int numPointsDeleted = _trackInfo.deleteMarkedPoints();
-               // add to undo stack if successful
-               if (numPointsDeleted > 0)
-               {
-                       undo.setNumPointsDeleted(numPointsDeleted);
-                       _undoStack.add(undo);
-                       UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
-                                + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
-               }
-               else {
-                       showErrorMessage("function.compress", "dialog.compress.nonefound");
-               }
-       }
-
        /**
         * Reverse the currently selected section of the track
         */
@@ -443,16 +402,16 @@ public class App
        }
 
        /**
-        * Complete the add time offset function with the specified offset
+        * Complete the add time offset function with the specified offset in seconds
         * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
         */
-       public void finishAddTimeOffset(long inTimeOffset)
+       public void finishAddTimeOffsetSeconds(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))
+               if (_trackInfo.getTrack().addTimeOffsetSeconds(selStart, selEnd, inTimeOffset, false))
                {
                        _undoStack.add(undo);
                        UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
@@ -464,12 +423,12 @@ public class App
        /**
         * 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)
+        * @param inUnit altitude units of offset (eg Feet, Metres)
         */
-       public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
+       public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
        {
                // Sanity check
-               if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
+               if (inOffset == null || inOffset.equals("") || inUnit == null) {
                        return;
                }
                // Construct undo information
@@ -482,7 +441,7 @@ public class App
                // Decimal offset given
                try {
                        double offsetd = Double.parseDouble(inOffset);
-                       success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
+                       success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
                }
                catch (NumberFormatException nfe) {}
                if (success)
@@ -518,29 +477,6 @@ public class App
        }
 
 
-       /**
-        * Interpolate the two selected points
-        */
-       public void interpolateSelection()
-       {
-               // Get number of points to add
-               Object numPointsStr = JOptionPane.showInputDialog(_frame,
-                       I18nManager.getText("dialog.interpolate.parameter.text"),
-                       I18nManager.getText("dialog.interpolate.title"),
-                       JOptionPane.QUESTION_MESSAGE, null, null, "");
-               int numPoints = parseNumber(numPointsStr);
-               if (numPoints <= 0) return;
-
-               UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
-                       numPoints);
-               // call track to interpolate
-               if (_trackInfo.interpolate(numPoints))
-               {
-                       _undoStack.add(undo);
-               }
-       }
-
-
        /**
         * Average the selected points
         */
@@ -560,16 +496,26 @@ public class App
 
 
        /**
-        * Create a new point at the given position
+        * Create a new point at the end of the track
         * @param inPoint point to add
         */
        public void createPoint(DataPoint inPoint)
+       {
+               createPoint(inPoint, true);
+       }
+
+       /**
+        * Create a new point at the end of the track
+        * @param inPoint point to add
+        * @param inNewSegment true for a single point, false for a continuation
+        */
+       public void createPoint(DataPoint inPoint, boolean inNewSegment)
        {
                // create undo object
                UndoCreatePoint undo = new UndoCreatePoint();
                _undoStack.add(undo);
                // add point to track
-               inPoint.setSegmentStart(true);
+               inPoint.setSegmentStart(inNewSegment);
                _track.appendPoints(new DataPoint[] {inPoint});
                // ensure track's field list contains point's fields
                _track.extendFieldList(inPoint.getFieldList());
@@ -579,6 +525,33 @@ public class App
        }
 
 
+       /**
+        * Create a new point before the given position
+        * @param inPoint point to add
+        * @param inIndex index of following point
+        */
+       public void createPoint(DataPoint inPoint, int inIndex)
+       {
+               // create undo object
+               UndoInsert undo = new UndoInsert(inIndex, 1);
+               _undoStack.add(undo);
+               // add point to track
+               _track.insertPoint(inPoint, inIndex);
+               // ensure track's field list contains point's fields
+               _track.extendFieldList(inPoint.getFieldList());
+               _trackInfo.selectPoint(inIndex);
+               final int selStart = _trackInfo.getSelection().getStart();
+               final int selEnd   = _trackInfo.getSelection().getEnd();
+               if (selStart < inIndex && selEnd >= inIndex)
+               {
+                       // Extend end of selection by 1
+                       _trackInfo.getSelection().selectRange(selStart, selEnd+1);
+               }
+               // update listeners
+               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
+       }
+
+
        /**
         * Cut the current selection and move it to before the currently selected point
         */
@@ -629,32 +602,51 @@ public class App
        }
 
        /**
-        * Receive loaded data and start load
+        * 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 inSourceInfo information about the source of the data
+        * @param inTrackNameList information about the track names
         */
        public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
-               Altitude.Format inAltFormat, SourceInfo inSourceInfo)
+               SourceInfo inSourceInfo, TrackNameList inTrackNameList)
        {
-               informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo, null);
+               // no link array given
+               informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
+                       inTrackNameList, null);
        }
 
        /**
         * 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 inOptions creation options such as units
         * @param inSourceInfo information about the source of the data
         * @param inTrackNameList information about the track names
         */
        public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
-               Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
+               PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
+       {
+               // no link array given
+               informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
+                       inTrackNameList, null);
+       }
+
+       /**
+        * Receive loaded data and determine whether to filter on tracks or not
+        * @param inFieldArray array of fields
+        * @param inDataArray array of data
+        * @param inOptions creation options such as units
+        * @param inSourceInfo information about the source of the data
+        * @param inTrackNameList information about the track names
+        * @param inLinkInfo links to photo/audio clips
+        */
+       public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
+               SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
        {
                // Check whether loaded array can be properly parsed into a Track
                Track loadedTrack = new Track();
-               loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
+               loadedTrack.load(inFieldArray, inDataArray, inOptions);
                if (loadedTrack.getNumPoints() <= 0)
                {
                        showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
@@ -667,19 +659,30 @@ public class App
                        JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
                                I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
                }
+
+               _busyLoading = true;
+               // Attach photos and/or audio clips to points
+               if (inLinkInfo != null)
+               {
+                       String[] linkArray = inLinkInfo.getLinkArray();
+                       if (linkArray != null) {
+                               new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
+                       }
+               }
                // 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();
+                       new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
                }
                else {
                        // go directly to load
                        informDataLoaded(loadedTrack, inSourceInfo);
                }
+               setCurrentMode(AppMode.NORMAL);
        }
 
+
        /**
         * Receive loaded data and optionally merge with current Track
         * @param inLoadedTrack loaded track
@@ -692,20 +695,25 @@ public class App
                {
                        // ask whether to replace or append
                        int answer = 0;
-                       if (_dataFiles == null || _firstDataFile) {
+                       if (_autoAppendNextFile) {
+                               // Automatically append the next file
+                               answer = JOptionPane.YES_OPTION;
+                       }
+                       else {
+                               // Ask whether to append or not
                                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;
-                       }
+                       _autoAppendNextFile = false; // reset flag to cancel autoappend
+
                        if (answer == JOptionPane.YES_OPTION)
                        {
                                // append data to current Track
-                               _undoStack.add(new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints()));
+                               UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
+                               undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
+                               _undoStack.add(undo);
                                _track.combine(inLoadedTrack);
                                // set source information
                                inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
@@ -718,33 +726,41 @@ public class App
                                if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
                                        photos = _trackInfo.getPhotoList().cloneList();
                                }
-                               _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos));
+                               UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
+                               undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
+                               _undoStack.add(undo);
                                _lastSavePosition = _undoStack.size();
                                _trackInfo.getSelection().clearAll();
                                _track.load(inLoadedTrack);
                                inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
                                _trackInfo.getFileInfo().replaceSource(inSourceInfo);
-                               if (photos != null) {
-                                       _trackInfo.getPhotoList().removeCorrelatedPhotos();
-                               }
+                               _trackInfo.getPhotoList().removeCorrelatedPhotos();
+                               _trackInfo.getAudioList().removeCorrelatedAudios();
                        }
                }
                else
                {
                        // Currently no data held, so transfer received data
-                       _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null));
+                       UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
+                       undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
+                       _undoStack.add(undo);
                        _lastSavePosition = _undoStack.size();
                        _trackInfo.getSelection().clearAll();
                        _track.load(inLoadedTrack);
                        inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
                        _trackInfo.getFileInfo().addSource(inSourceInfo);
                }
+               // Update config before subscribers are told
+               boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
+               Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
                UpdateMessageBroker.informSubscribers();
                // Update status bar
                UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
                        + " '" + inSourceInfo.getName() + "'");
                // update menu
                _menuManager.informFileLoaded();
+               // Remove busy lock
+               _busyLoading = false;
                // load next file if there's a queue
                loadNextFile();
        }
@@ -759,12 +775,20 @@ public class App
                loadNextFile();
        }
 
+       /**
+        * External trigger to automatically append the next loaded file
+        * instead of prompting to replace or append
+        */
+       public void autoAppendNextFile()
+       {
+               _autoAppendNextFile = true;
+       }
+
        /**
         * Load the next file in the waiting list, if any
         */
        private void loadNextFile()
        {
-               _firstDataFile = false;
                if (_dataFiles == null || _dataFiles.size() == 0) {
                        _dataFiles = null;
                }
@@ -773,6 +797,7 @@ public class App
                                public void run() {
                                        File f = _dataFiles.get(0);
                                        _dataFiles.remove(0);
+                                       _autoAppendNextFile = true;
                                        _fileLoader.openFile(f);
                                }
                        }).start();
@@ -805,92 +830,7 @@ public class App
                        // MAYBE: Improve message when photo(s) fail to load (eg already added)
                        UpdateMessageBroker.informSubscribers();
                        // update menu
-                       _menuManager.informFileLoaded();
-               }
-       }
-
-
-       /**
-        * Connect the current photo to the current point
-        */
-       public void connectPhotoToPoint()
-       {
-               Photo photo = _trackInfo.getCurrentPhoto();
-               DataPoint point = _trackInfo.getCurrentPoint();
-               if (photo != null && point != null)
-               {
-                       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"));
-                       }
-               }
-       }
-
-
-       /**
-        * 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);
-                       UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
-                       UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
-               }
-       }
-
-
-       /**
-        * Remove the current photo, if any
-        */
-       public void deleteCurrentPhoto()
-       {
-               // Delete the current photo, and optionally its point too, keeping undo information
-               Photo currentPhoto = _trackInfo.getCurrentPhoto();
-               if (currentPhoto != null)
-               {
-                       // Photo is selected, see if it has a point or not
-                       boolean photoDeleted = false;
-                       UndoDeletePhoto undoAction = null;
-                       if (currentPhoto.getDataPoint() == null)
-                       {
-                               // no point attached, so just delete photo
-                               undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
-                                       null, -1);
-                               photoDeleted = _trackInfo.deleteCurrentPhoto(false);
-                       }
-                       else
-                       {
-                               // point is attached, so need to confirm point deletion
-                               undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
-                                       currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
-                               int response = JOptionPane.showConfirmDialog(_frame,
-                                       I18nManager.getText("dialog.deletephoto.deletepoint"),
-                                       I18nManager.getText("dialog.deletephoto.title"),
-                                       JOptionPane.YES_NO_CANCEL_OPTION);
-                               boolean deletePointToo = (response == JOptionPane.YES_OPTION);
-                               // Cancel delete if cancel pressed or dialog closed
-                               if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
-                               {
-                                       photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
-                               }
-                       }
-                       // Add undo information to stack if necessary
-                       if (photoDeleted)
-                       {
-                               _undoStack.add(undoAction);
-                       }
+                       if (numPointsAdded > 0) _menuManager.informFileLoaded();
                }
        }
 
@@ -927,7 +867,7 @@ public class App
                }
                else
                {
-                       new UndoManager(this, _frame);
+                       new UndoManager(this, _frame).show();
                }
        }
 
@@ -967,7 +907,7 @@ public class App
                {
                        for (int i=0; i<inNumUndos; i++)
                        {
-                               _undoStack.pop().performUndo(_trackInfo);
+                               _undoStack.popOperation().performUndo(_trackInfo);
                        }
                        String message = "" + inNumUndos + " "
                                 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
@@ -978,40 +918,19 @@ public class App
                        showErrorMessageNoLookup("error.undofailed.title",
                                I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
                        _undoStack.clear();
-                       UpdateMessageBroker.informSubscribers();
                }
                catch (EmptyStackException empty) {}
+               UpdateMessageBroker.informSubscribers();
        }
 
-
        /**
-        * Helper method to parse an Object into an integer
-        * @param inObject object, eg from dialog
-        * @return int value given
+        * @return the current data status, used for later comparison
         */
-       private static int parseNumber(Object inObject)
+       public DataStatus getCurrentDataStatus()
        {
-               int num = 0;
-               if (inObject != null)
-               {
-                       try
-                       {
-                               num = Integer.parseInt(inObject.toString());
-                       }
-                       catch (NumberFormatException nfe)
-                       {}
-               }
-               return num;
+               return new DataStatus(_undoStack.size(), _undoStack.getNumUndos());
        }
 
-       /**
-        * 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
@@ -1057,7 +976,7 @@ public class App
         */
        public void setSidebarController(SidebarController inController)
        {
-               __sidebarController = inController;
+               _sidebarController = inController;
        }
 
        /**
@@ -1065,6 +984,21 @@ public class App
         */
        public void toggleSidebars()
        {
-               __sidebarController.toggle();
+               _sidebarController.toggle();
+       }
+
+       /** @return true if App is currently busy with loading data */
+       public boolean isBusyLoading() {
+               return _busyLoading;
+       }
+
+       /** @return current app mode */
+       public AppMode getCurrentMode() {
+               return _appMode;
+       }
+
+       /** @param inMode the current app mode */
+       public void setCurrentMode(AppMode inMode) {
+               _appMode = inMode;
        }
 }