]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/App.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / tim / prune / App.java
diff --git a/tim/prune/App.java b/tim/prune/App.java
deleted file mode 100644 (file)
index 3a77858..0000000
+++ /dev/null
@@ -1,1004 +0,0 @@
-package tim.prune;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.EmptyStackException;
-import java.util.Set;
-
-import javax.swing.JFrame;
-import javax.swing.JOptionPane;
-
-import tim.prune.config.Config;
-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.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.edit.FieldEditList;
-import tim.prune.function.edit.PointEditor;
-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.*;
-
-
-/**
- * Main controller for the application
- */
-public class App
-{
-       // Instance variables
-       private JFrame _frame = null;
-       private Track _track = null;
-       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 UndoStack _undoStack = null;
-       private ColourerCaretaker _colCaretaker = null;
-       private boolean _mangleTimestampsConfirmed = false;
-       private Viewport _viewport = null;
-       private ArrayList<File> _dataFiles = null;
-       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};
-
-
-       /**
-        * Constructor
-        * @param inFrame frame object for application
-        */
-       public App(JFrame inFrame)
-       {
-               _frame = inFrame;
-               _undoStack = new UndoStack();
-               _track = new Track();
-               _trackInfo = new TrackInfo(_track);
-               FunctionLibrary.initialise(this);
-               _colCaretaker = new ColourerCaretaker(this);
-               UpdateMessageBroker.addSubscriber(_colCaretaker);
-               _colCaretaker.setColourer(Config.getPointColourer());
-       }
-
-
-       /**
-        * @return the current TrackInfo
-        */
-       public TrackInfo getTrackInfo()
-       {
-               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
-        */
-       public boolean hasDataUnsaved()
-       {
-               return (_undoStack.size() > _lastSavePosition
-                       && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
-       }
-
-       /**
-        * @return the undo stack
-        */
-       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
-        */
-       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);
-                       _autoAppendNextFile = false; // prompt for append
-                       _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);
-               setCurrentMode(AppMode.NORMAL);
-       }
-
-       /**
-        * Set the MenuManager object to be informed about changes
-        * @param inManager MenuManager object
-        */
-       public void setMenuManager(MenuManager inManager)
-       {
-               _menuManager = inManager;
-       }
-
-
-       /**
-        * Open a file containing track or waypoint data
-        */
-       public void openFile()
-       {
-               if (_fileLoader == null)
-                       _fileLoader = new FileLoader(this, _frame);
-               _fileLoader.openFile();
-       }
-
-
-       /**
-        * Add a photo or a directory of photos
-        */
-       public void addPhotos()
-       {
-               if (_jpegLoader == null)
-                       _jpegLoader = new JpegLoader(this, _frame);
-               _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
-       }
-
-       /**
-        * Save the file in the selected format
-        */
-       public void saveFile()
-       {
-               if (_track == null) {
-                       showErrorMessage("error.save.dialogtitle", "error.save.nodata");
-               }
-               else
-               {
-                       if (_fileSaver == null) {
-                               _fileSaver = new FileSaver(this, _frame);
-                       }
-                       char delim = ',';
-                       if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
-                       _fileSaver.showDialog(delim);
-               }
-       }
-
-
-       /**
-        * Exit the application if confirmed
-        */
-       public void exit()
-       {
-               // grab focus
-               _frame.toFront();
-               _frame.requestFocus();
-               // check if ok to exit
-               Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
-               if (!hasDataUnsaved()
-                       || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
-                               I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
-                               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);
-               }
-       }
-
-
-       /**
-        * Edit the currently selected point
-        */
-       public void editCurrentPoint()
-       {
-               if (_track != null)
-               {
-                       DataPoint currentPoint = _trackInfo.getCurrentPoint();
-                       if (currentPoint != null)
-                       {
-                               // Open point dialog to display details
-                               PointEditor editor = new PointEditor(this, _frame);
-                               editor.showDialog(_track, currentPoint);
-                       }
-               }
-       }
-
-
-       /**
-        * Complete the point edit
-        * @param inEditList field values to edit
-        * @param inUndoList field values before edit
-        */
-       public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
-       {
-               DataPoint currentPoint = _trackInfo.getCurrentPoint();
-               if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
-               {
-                       // add information to undo stack
-                       UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
-                       // pass to track for completion
-                       if (_track.editPoint(currentPoint, inEditList, false))
-                       {
-                               _undoStack.add(undo);
-                               // Confirm point edit
-                               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
-                       }
-               }
-       }
-
-
-       /**
-        * Delete the currently selected point
-        */
-       public void deleteCurrentPoint()
-       {
-               if (_track == null) {return;}
-               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.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);
-                       int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
-                       DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
-                       // Construct Undo object
-                       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.add(undo);
-                               if (currentPhoto != null)
-                               {
-                                       // delete photo if necessary
-                                       if (deletePhoto)
-                                       {
-                                               _trackInfo.getPhotoList().deletePhoto(photoIndex);
-                                       }
-                                       else
-                                       {
-                                               // 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);
-                       }
-               }
-       }
-
-
-       /**
-        * Reverse the currently selected section of the track
-        */
-       public void reverseRange()
-       {
-               // check whether Timestamp field exists, and if so confirm reversal
-               int selStart = _trackInfo.getSelection().getStart();
-               int selEnd = _trackInfo.getSelection().getEnd();
-               if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
-                       || _mangleTimestampsConfirmed
-                       || (JOptionPane.showConfirmDialog(_frame,
-                                I18nManager.getText("dialog.confirmreversetrack.text"),
-                                I18nManager.getText("dialog.confirmreversetrack.title"),
-                                JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
-               {
-                       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 in seconds
-        * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
-        */
-       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().addTimeOffsetSeconds(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 inUnit altitude units of offset (eg Feet, Metres)
-        */
-       public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
-       {
-               // Sanity check
-               if (inOffset == null || inOffset.equals("") || inUnit == null) {
-                       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, inUnit, 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"));
-                       }
-               }
-       }
-
-
-       /**
-        * Average the selected points
-        */
-       public void averageSelection()
-       {
-               // 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);
-               }
-       }
-
-
-       /**
-        * 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(inNewSegment);
-               _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"));
-       }
-
-
-       /**
-        * 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
-        */
-       public void cutAndMoveSelection()
-       {
-               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"));
-                       }
-               }
-       }
-
-       /**
-        * Select nothing
-        */
-       public void selectNone()
-       {
-               // deselect point, range and photo
-               _trackInfo.getSelection().clearAll();
-               _track.clearDeletionMarkers();
-       }
-
-       /**
-        * Receive loaded data and determine whether to filter on tracks or not
-        * @param inFieldArray array of fields
-        * @param inDataArray array of data
-        * @param inSourceInfo information about the source of the data
-        * @param inTrackNameList information about the track names
-        */
-       public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
-               SourceInfo inSourceInfo, TrackNameList inTrackNameList)
-       {
-               // 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 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,
-               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, inOptions);
-               if (loadedTrack.getNumPoints() <= 0)
-               {
-                       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);
-               }
-
-               _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, 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
-        * @param inSourceInfo information about the source of the data
-        */
-       public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
-       {
-               // Decide whether to load or append
-               if (_track.getNumPoints() > 0)
-               {
-                       // ask whether to replace or append
-                       int answer = 0;
-                       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);
-                       }
-                       _autoAppendNextFile = false; // reset flag to cancel autoappend
-
-                       if (answer == JOptionPane.YES_OPTION)
-                       {
-                               // append data to current Track
-                               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());
-                               _trackInfo.getFileInfo().addSource(inSourceInfo);
-                       }
-                       else if (answer == JOptionPane.NO_OPTION)
-                       {
-                               // Don't append, replace data
-                               PhotoList photos = null;
-                               if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
-                                       photos = _trackInfo.getPhotoList().cloneList();
-                               }
-                               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);
-                               _trackInfo.getPhotoList().removeCorrelatedPhotos();
-                               _trackInfo.getAudioList().removeCorrelatedAudios();
-                       }
-               }
-               else
-               {
-                       // Currently no data held, so transfer received data
-                       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();
-       }
-
-       /**
-        * 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();
-       }
-
-       /**
-        * 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()
-       {
-               if (_dataFiles == null || _dataFiles.size() == 0) {
-                       _dataFiles = null;
-               }
-               else {
-                       new Thread(new Runnable() {
-                               public void run() {
-                                       File f = _dataFiles.get(0);
-                                       _dataFiles.remove(0);
-                                       _autoAppendNextFile = true;
-                                       _fileLoader.openFile(f);
-                               }
-                       }).start();
-               }
-       }
-
-
-       /**
-        * Accept a list of loaded photos
-        * @param inPhotoSet Set of Photo objects
-        */
-       public void informPhotosLoaded(Set<Photo> inPhotoSet)
-       {
-               if (inPhotoSet != null && !inPhotoSet.isEmpty())
-               {
-                       int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
-                       int numPhotosAdded = numsAdded[0];
-                       int numPointsAdded = numsAdded[1];
-                       if (numPhotosAdded > 0)
-                       {
-                               // Save numbers so load can be undone
-                               _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
-                       }
-                       if (numPhotosAdded == 1) {
-                               UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
-                       }
-                       else {
-                               UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
-                       }
-                       // MAYBE: Improve message when photo(s) fail to load (eg already added)
-                       UpdateMessageBroker.informSubscribers();
-                       // update menu
-                       if (numPointsAdded > 0) _menuManager.informFileLoaded();
-               }
-       }
-
-
-       /**
-        * Save the coordinates of photos in their exif data
-        */
-       public void saveExif()
-       {
-               ExifSaver saver = new ExifSaver(_frame);
-               saver.saveExifInformation(_trackInfo.getPhotoList());
-       }
-
-
-       /**
-        * Inform the app that the data has been saved
-        */
-       public void informDataSaved()
-       {
-               _lastSavePosition = _undoStack.size();
-       }
-
-
-       /**
-        * Begin undo process
-        */
-       public void beginUndo()
-       {
-               if (_undoStack.isEmpty())
-               {
-                       // Nothing to undo
-                       JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
-                               I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
-               }
-               else
-               {
-                       new UndoManager(this, _frame).show();
-               }
-       }
-
-
-       /**
-        * Clear the undo stack (losing all undo information
-        */
-       public void clearUndo()
-       {
-               // Exit if nothing to undo
-               if (_undoStack == null || _undoStack.isEmpty())
-                       return;
-               // Has track got unsaved data?
-               boolean unsaved = hasDataUnsaved();
-               // Confirm operation with dialog
-               int answer = JOptionPane.showConfirmDialog(_frame,
-                       I18nManager.getText("dialog.clearundo.text"),
-                       I18nManager.getText("dialog.clearundo.title"),
-                       JOptionPane.YES_NO_OPTION);
-               if (answer == JOptionPane.YES_OPTION)
-               {
-                       _undoStack.clear();
-                       _lastSavePosition = 0;
-                       if (unsaved) _lastSavePosition = -1;
-                       UpdateMessageBroker.informSubscribers();
-               }
-       }
-
-
-       /**
-        * Undo the specified number of actions
-        * @param inNumUndos number of actions to undo
-        */
-       public void undoActions(int inNumUndos)
-       {
-               try
-               {
-                       for (int i=0; i<inNumUndos; i++)
-                       {
-                               _undoStack.popOperation().performUndo(_trackInfo);
-                       }
-                       String message = "" + inNumUndos + " "
-                                + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
-                       UpdateMessageBroker.informSubscribers(message);
-               }
-               catch (UndoException ue)
-               {
-                       showErrorMessageNoLookup("error.undofailed.title",
-                               I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
-                       _undoStack.clear();
-               }
-               catch (EmptyStackException empty) {}
-               UpdateMessageBroker.informSubscribers();
-       }
-
-       /**
-        * @return the current data status, used for later comparison
-        */
-       public DataStatus getCurrentDataStatus()
-       {
-               return new DataStatus(_undoStack.size(), _undoStack.getNumUndos());
-       }
-
-
-       /**
-        * 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 toggleSidebars()
-       {
-               _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;
-       }
-}