X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=src%2Ftim%2Fprune%2FApp.java;fp=src%2Ftim%2Fprune%2FApp.java;h=3a778588ab815a7f30c4b396e4de9d2c266836da;hp=0000000000000000000000000000000000000000;hb=ce6f2161b8596f7018d6a76bff79bc9e571f35fd;hpb=2d8cb72e84d5cc1089ce77baf1e34ea3ea2f8465 diff --git a/src/tim/prune/App.java b/src/tim/prune/App.java new file mode 100644 index 0000000..3a77858 --- /dev/null +++ b/src/tim/prune/App.java @@ -0,0 +1,1004 @@ +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 _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 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 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