+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;
+ }
+}