4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
7 import java.util.Stack;
9 import javax.swing.JFrame;
10 import javax.swing.JOptionPane;
12 import tim.prune.config.Config;
13 import tim.prune.data.Checker;
14 import tim.prune.data.DataPoint;
15 import tim.prune.data.Field;
16 import tim.prune.data.LatLonRectangle;
17 import tim.prune.data.NumberUtils;
18 import tim.prune.data.Photo;
19 import tim.prune.data.PhotoList;
20 import tim.prune.data.PointCreateOptions;
21 import tim.prune.data.RecentFile;
22 import tim.prune.data.SourceInfo;
23 import tim.prune.data.Track;
24 import tim.prune.data.TrackInfo;
25 import tim.prune.data.SourceInfo.FILE_TYPE;
26 import tim.prune.data.Unit;
27 import tim.prune.function.AsyncMediaLoader;
28 import tim.prune.function.SaveConfig;
29 import tim.prune.function.SelectTracksFunction;
30 import tim.prune.function.browser.BrowserLauncher;
31 import tim.prune.function.browser.UrlGenerator;
32 import tim.prune.function.edit.FieldEditList;
33 import tim.prune.function.edit.PointEditor;
34 import tim.prune.gui.MenuManager;
35 import tim.prune.gui.SidebarController;
36 import tim.prune.gui.UndoManager;
37 import tim.prune.gui.Viewport;
38 import tim.prune.gui.colour.ColourerCaretaker;
39 import tim.prune.gui.colour.PointColourer;
40 import tim.prune.load.FileLoader;
41 import tim.prune.load.JpegLoader;
42 import tim.prune.load.MediaLinkInfo;
43 import tim.prune.load.TrackNameList;
44 import tim.prune.save.ExifSaver;
45 import tim.prune.save.FileSaver;
46 import tim.prune.tips.TipManager;
47 import tim.prune.undo.*;
51 * Main controller for the application
56 private JFrame _frame = null;
57 private Track _track = null;
58 private TrackInfo _trackInfo = null;
59 private int _lastSavePosition = 0;
60 private MenuManager _menuManager = null;
61 private SidebarController _sidebarController = null;
62 private FileLoader _fileLoader = null;
63 private JpegLoader _jpegLoader = null;
64 private FileSaver _fileSaver = null;
65 private UndoStack _undoStack = null;
66 private ColourerCaretaker _colCaretaker = null;
67 private boolean _mangleTimestampsConfirmed = false;
68 private Viewport _viewport = null;
69 private ArrayList<File> _dataFiles = null;
70 private boolean _autoAppendNextFile = false;
71 private boolean _busyLoading = false;
72 private AppMode _appMode = AppMode.NORMAL;
74 /** Enum for the app mode - currently only two options but may expand later */
75 public enum AppMode {NORMAL, DRAWRECT};
80 * @param inFrame frame object for application
82 public App(JFrame inFrame)
85 _undoStack = new UndoStack();
87 _trackInfo = new TrackInfo(_track);
88 FunctionLibrary.initialise(this);
89 _colCaretaker = new ColourerCaretaker(this);
90 UpdateMessageBroker.addSubscriber(_colCaretaker);
91 _colCaretaker.setColourer(Config.getPointColourer());
96 * @return the current TrackInfo
98 public TrackInfo getTrackInfo()
104 * @return the dialog frame
106 public JFrame getFrame()
112 * Check if the application has unsaved data
113 * @return true if data is unsaved, false otherwise
115 public boolean hasDataUnsaved()
117 return (_undoStack.size() > _lastSavePosition
118 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
122 * @return the undo stack
124 public Stack<UndoOperation> getUndoStack()
130 * Update the system's point colourer using the one in the Config
132 public void updatePointColourer()
134 if (_colCaretaker != null) {
135 _colCaretaker.setColourer(Config.getPointColourer());
140 * @return colourer object, or null
142 public PointColourer getPointColourer()
144 if (_colCaretaker == null) {return null;}
145 return _colCaretaker.getColourer();
149 * Show the specified tip if appropriate
150 * @param inTipNumber tip number from TipManager
152 public void showTip(int inTipNumber)
154 String key = TipManager.fireTipTrigger(inTipNumber);
155 if (key != null && !key.equals(""))
157 JOptionPane.showMessageDialog(_frame, I18nManager.getText(key),
158 I18nManager.getText("tip.title"), JOptionPane.INFORMATION_MESSAGE);
164 * Load the specified data files one by one
165 * @param inDataFiles arraylist containing File objects to load
167 public void loadDataFiles(ArrayList<File> inDataFiles)
169 if (inDataFiles == null || inDataFiles.size() == 0) {
174 _dataFiles = inDataFiles;
175 File f = _dataFiles.get(0);
176 _dataFiles.remove(0);
177 // Start load of specified file
178 if (_fileLoader == null)
179 _fileLoader = new FileLoader(this, _frame);
180 _autoAppendNextFile = false; // prompt for append
181 _fileLoader.openFile(f);
186 * Complete a function execution
187 * @param inUndo undo object to be added to stack
188 * @param inConfirmText confirmation text
190 public void completeFunction(UndoOperation inUndo, String inConfirmText)
192 _undoStack.add(inUndo);
193 UpdateMessageBroker.informSubscribers(inConfirmText);
194 setCurrentMode(AppMode.NORMAL);
198 * Set the MenuManager object to be informed about changes
199 * @param inManager MenuManager object
201 public void setMenuManager(MenuManager inManager)
203 _menuManager = inManager;
208 * Open a file containing track or waypoint data
210 public void openFile()
212 if (_fileLoader == null)
213 _fileLoader = new FileLoader(this, _frame);
214 _fileLoader.openFile();
219 * Add a photo or a directory of photos
221 public void addPhotos()
223 if (_jpegLoader == null)
224 _jpegLoader = new JpegLoader(this, _frame);
225 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
229 * Save the file in the selected format
231 public void saveFile()
233 if (_track == null) {
234 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
238 if (_fileSaver == null) {
239 _fileSaver = new FileSaver(this, _frame);
242 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
243 _fileSaver.showDialog(delim);
249 * Exit the application if confirmed
255 _frame.requestFocus();
256 // check if ok to exit
257 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
258 if (!hasDataUnsaved()
259 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
260 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
261 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
262 == JOptionPane.YES_OPTION)
265 if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
266 new SaveConfig(this).silentSave();
274 * Edit the currently selected point
276 public void editCurrentPoint()
280 DataPoint currentPoint = _trackInfo.getCurrentPoint();
281 if (currentPoint != null)
283 // Open point dialog to display details
284 PointEditor editor = new PointEditor(this, _frame);
285 editor.showDialog(_track, currentPoint);
292 * Complete the point edit
293 * @param inEditList field values to edit
294 * @param inUndoList field values before edit
296 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
298 DataPoint currentPoint = _trackInfo.getCurrentPoint();
299 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
301 // add information to undo stack
302 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
303 // pass to track for completion
304 if (_track.editPoint(currentPoint, inEditList, false))
306 _undoStack.push(undo);
307 // Confirm point edit
308 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
315 * Delete the currently selected point
317 public void deleteCurrentPoint()
319 if (_track == null) {return;}
320 DataPoint currentPoint = _trackInfo.getCurrentPoint();
321 if (currentPoint != null)
324 boolean deletePhoto = false;
325 Photo currentPhoto = currentPoint.getPhoto();
326 if (currentPhoto != null)
328 // Confirm deletion of photo or decoupling
329 int response = JOptionPane.showConfirmDialog(_frame,
330 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
331 I18nManager.getText("dialog.deletepoint.title"),
332 JOptionPane.YES_NO_CANCEL_OPTION);
333 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
335 // cancel pressed- abort delete
338 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
340 // store necessary information to undo it later
341 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
342 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
343 int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
344 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
345 // Construct Undo object
346 UndoDeletePoint undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
347 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
348 undo.setAtBoundaryOfSelectedRange(pointIndex == _trackInfo.getSelection().getStart() ||
349 pointIndex == _trackInfo.getSelection().getEnd());
350 // call track to delete point
351 if (_trackInfo.deletePoint())
353 // Delete was successful so add undo info to stack
354 _undoStack.push(undo);
355 if (currentPhoto != null)
357 // delete photo if necessary
360 _trackInfo.getPhotoList().deletePhoto(photoIndex);
364 // decouple photo from point
365 currentPhoto.setDataPoint(null);
367 UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
369 // Delete audio object (without bothering to ask)
370 if (audioIndex > -1) {
371 _trackInfo.getAudioList().deleteAudio(audioIndex);
374 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
375 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
382 * Finish the compression by deleting the marked points
384 public void finishCompressTrack()
386 UndoDeleteMarked undo = new UndoDeleteMarked(_track);
387 // call track to do compress
388 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
389 // add to undo stack if successful
390 if (numPointsDeleted > 0)
392 undo.setNumPointsDeleted(numPointsDeleted);
393 _undoStack.add(undo);
394 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
395 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
398 showErrorMessage("function.compress", "dialog.deletemarked.nonefound");
403 * Reverse the currently selected section of the track
405 public void reverseRange()
407 // check whether Timestamp field exists, and if so confirm reversal
408 int selStart = _trackInfo.getSelection().getStart();
409 int selEnd = _trackInfo.getSelection().getEnd();
410 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
411 || _mangleTimestampsConfirmed
412 || (JOptionPane.showConfirmDialog(_frame,
413 I18nManager.getText("dialog.confirmreversetrack.text"),
414 I18nManager.getText("dialog.confirmreversetrack.title"),
415 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
417 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
418 // call track to reverse range
419 if (_track.reverseRange(selStart, selEnd))
421 _undoStack.add(undo);
423 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
429 * Complete the add time offset function with the specified offset
430 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
432 public void finishAddTimeOffset(long inTimeOffset)
434 // Construct undo information
435 int selStart = _trackInfo.getSelection().getStart();
436 int selEnd = _trackInfo.getSelection().getEnd();
437 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
438 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
440 _undoStack.add(undo);
441 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
442 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
448 * Complete the add altitude offset function with the specified offset
449 * @param inOffset altitude offset to add as String
450 * @param inUnit altitude units of offset (eg Feet, Metres)
452 public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
455 if (inOffset == null || inOffset.equals("") || inUnit == null) {
458 // Construct undo information
459 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
460 int selStart = _trackInfo.getSelection().getStart();
461 int selEnd = _trackInfo.getSelection().getEnd();
462 // How many decimal places are given in the offset?
463 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
464 boolean success = false;
465 // Decimal offset given
467 double offsetd = Double.parseDouble(inOffset);
468 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
470 catch (NumberFormatException nfe) {}
473 _undoStack.add(undo);
474 _trackInfo.getSelection().markInvalid();
475 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
476 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
482 * Merge the track segments within the current selection
484 public void mergeTrackSegments()
486 if (_trackInfo.getSelection().hasRangeSelected())
488 // Maybe could check segment start flags to see if it's worth merging
489 // If first track point is already start and no other seg starts then do nothing
491 int selStart = _trackInfo.getSelection().getStart();
492 int selEnd = _trackInfo.getSelection().getEnd();
494 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
495 // Call track to merge segments
496 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
497 _undoStack.add(undo);
498 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
505 * Average the selected points
507 public void averageSelection()
509 // Find following track point
510 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
511 boolean segFlag = false;
512 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
513 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
514 // call track info object to do the averaging
515 if (_trackInfo.average())
517 _undoStack.add(undo);
523 * Create a new point at the end of the track
524 * @param inPoint point to add
526 public void createPoint(DataPoint inPoint)
528 createPoint(inPoint, true);
532 * Create a new point at the end of the track
533 * @param inPoint point to add
534 * @param inNewSegment true for a single point, false for a continuation
536 public void createPoint(DataPoint inPoint, boolean inNewSegment)
538 // create undo object
539 UndoCreatePoint undo = new UndoCreatePoint();
540 _undoStack.add(undo);
541 // add point to track
542 inPoint.setSegmentStart(inNewSegment);
543 _track.appendPoints(new DataPoint[] {inPoint});
544 // ensure track's field list contains point's fields
545 _track.extendFieldList(inPoint.getFieldList());
546 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
548 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
553 * Create a new point before the given position
554 * @param inPoint point to add
555 * @param inIndex index of following point
557 public void createPoint(DataPoint inPoint, int inIndex)
559 // create undo object
560 UndoInsert undo = new UndoInsert(inIndex, 1);
561 _undoStack.add(undo);
562 // add point to track
563 _track.insertPoint(inPoint, inIndex);
564 // ensure track's field list contains point's fields
565 _track.extendFieldList(inPoint.getFieldList());
566 _trackInfo.selectPoint(inIndex);
567 final int selStart = _trackInfo.getSelection().getStart();
568 final int selEnd = _trackInfo.getSelection().getEnd();
569 if (selStart < inIndex && selEnd >= inIndex)
571 // Extend end of selection by 1
572 _trackInfo.getSelection().selectRange(selStart, selEnd+1);
575 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
580 * Cut the current selection and move it to before the currently selected point
582 public void cutAndMoveSelection()
584 int startIndex = _trackInfo.getSelection().getStart();
585 int endIndex = _trackInfo.getSelection().getEnd();
586 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
587 // If timestamps would be mangled by cut/move, confirm
588 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
589 || _mangleTimestampsConfirmed
590 || (JOptionPane.showConfirmDialog(_frame,
591 I18nManager.getText("dialog.confirmcutandmove.text"),
592 I18nManager.getText("dialog.confirmcutandmove.title"),
593 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
595 // Find points to set segment flags
596 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
597 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
598 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
600 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
601 // Call track info to move track section
602 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
604 // Set segment start flags (first track point, next track point, move to point)
605 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
606 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
607 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
609 // Add undo object to stack, set confirm message
610 _undoStack.add(undo);
611 _trackInfo.getSelection().selectRange(-1, -1);
612 UpdateMessageBroker.informSubscribers();
613 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
621 public void selectNone()
623 // deselect point, range and photo
624 _trackInfo.getSelection().clearAll();
625 _track.clearDeletionMarkers();
629 * Receive loaded data and determine whether to filter on tracks or not
630 * @param inFieldArray array of fields
631 * @param inDataArray array of data
632 * @param inSourceInfo information about the source of the data
633 * @param inTrackNameList information about the track names
635 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
636 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
638 // no link array given
639 informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
640 inTrackNameList, null);
644 * Receive loaded data and determine whether to filter on tracks or not
645 * @param inFieldArray array of fields
646 * @param inDataArray array of data
647 * @param inOptions creation options such as units
648 * @param inSourceInfo information about the source of the data
649 * @param inTrackNameList information about the track names
651 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
652 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
654 // no link array given
655 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
656 inTrackNameList, null);
660 * Receive loaded data and determine whether to filter on tracks or not
661 * @param inFieldArray array of fields
662 * @param inDataArray array of data
663 * @param inOptions creation options such as units
664 * @param inSourceInfo information about the source of the data
665 * @param inTrackNameList information about the track names
666 * @param inLinkInfo links to photo/audio clips
668 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
669 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
671 // Check whether loaded array can be properly parsed into a Track
672 Track loadedTrack = new Track();
673 loadedTrack.load(inFieldArray, inDataArray, inOptions);
674 if (loadedTrack.getNumPoints() <= 0)
676 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
677 // load next file if there's a queue
681 // Check for doubled track
682 if (Checker.isDoubledTrack(loadedTrack)) {
683 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
684 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
688 // Attach photos and/or audio clips to points
689 if (inLinkInfo != null)
691 String[] linkArray = inLinkInfo.getLinkArray();
692 if (linkArray != null) {
693 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
696 // Look at TrackNameList, decide whether to filter or not
697 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
699 // Launch a dialog to let the user choose which tracks to load, then continue
700 new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
703 // go directly to load
704 informDataLoaded(loadedTrack, inSourceInfo);
706 setCurrentMode(AppMode.NORMAL);
711 * Receive loaded data and optionally merge with current Track
712 * @param inLoadedTrack loaded track
713 * @param inSourceInfo information about the source of the data
715 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
717 // Decide whether to load or append
718 if (_track.getNumPoints() > 0)
720 // ask whether to replace or append
722 if (_autoAppendNextFile) {
723 // Automatically append the next file
724 answer = JOptionPane.YES_OPTION;
727 // Ask whether to append or not
728 answer = JOptionPane.showConfirmDialog(_frame,
729 I18nManager.getText("dialog.openappend.text"),
730 I18nManager.getText("dialog.openappend.title"),
731 JOptionPane.YES_NO_CANCEL_OPTION);
733 _autoAppendNextFile = false; // reset flag to cancel autoappend
735 if (answer == JOptionPane.YES_OPTION)
737 // append data to current Track
738 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
739 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
740 _undoStack.add(undo);
741 _track.combine(inLoadedTrack);
742 // set source information
743 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
744 _trackInfo.getFileInfo().addSource(inSourceInfo);
746 else if (answer == JOptionPane.NO_OPTION)
748 // Don't append, replace data
749 PhotoList photos = null;
750 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
751 photos = _trackInfo.getPhotoList().cloneList();
753 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
754 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
755 _undoStack.add(undo);
756 _lastSavePosition = _undoStack.size();
757 _trackInfo.getSelection().clearAll();
758 _track.load(inLoadedTrack);
759 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
760 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
761 _trackInfo.getPhotoList().removeCorrelatedPhotos();
762 _trackInfo.getAudioList().removeCorrelatedAudios();
767 // Currently no data held, so transfer received data
768 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
769 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
770 _undoStack.add(undo);
771 _lastSavePosition = _undoStack.size();
772 _trackInfo.getSelection().clearAll();
773 _track.load(inLoadedTrack);
774 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
775 _trackInfo.getFileInfo().addSource(inSourceInfo);
777 // Update config before subscribers are told
778 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
779 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
780 UpdateMessageBroker.informSubscribers();
782 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
783 + " '" + inSourceInfo.getName() + "'");
785 _menuManager.informFileLoaded();
787 _busyLoading = false;
788 // load next file if there's a queue
793 * Inform the app that NO data was loaded, eg cancel pressed
794 * Only needed if there's another file waiting in the queue
796 public void informNoDataLoaded()
798 // Load next file if there's a queue
803 * External trigger to automatically append the next loaded file
804 * instead of prompting to replace or append
806 public void autoAppendNextFile()
808 _autoAppendNextFile = true;
812 * Load the next file in the waiting list, if any
814 private void loadNextFile()
816 if (_dataFiles == null || _dataFiles.size() == 0) {
820 new Thread(new Runnable() {
822 File f = _dataFiles.get(0);
823 _dataFiles.remove(0);
824 _autoAppendNextFile = true;
825 _fileLoader.openFile(f);
833 * Accept a list of loaded photos
834 * @param inPhotoSet Set of Photo objects
836 public void informPhotosLoaded(Set<Photo> inPhotoSet)
838 if (inPhotoSet != null && !inPhotoSet.isEmpty())
840 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
841 int numPhotosAdded = numsAdded[0];
842 int numPointsAdded = numsAdded[1];
843 if (numPhotosAdded > 0)
845 // Save numbers so load can be undone
846 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
848 if (numPhotosAdded == 1) {
849 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
852 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
854 // MAYBE: Improve message when photo(s) fail to load (eg already added)
855 UpdateMessageBroker.informSubscribers();
857 if (numPointsAdded > 0) _menuManager.informFileLoaded();
863 * Save the coordinates of photos in their exif data
865 public void saveExif()
867 ExifSaver saver = new ExifSaver(_frame);
868 saver.saveExifInformation(_trackInfo.getPhotoList());
873 * Inform the app that the data has been saved
875 public void informDataSaved()
877 _lastSavePosition = _undoStack.size();
884 public void beginUndo()
886 if (_undoStack.isEmpty())
889 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
890 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
894 new UndoManager(this, _frame);
900 * Clear the undo stack (losing all undo information
902 public void clearUndo()
904 // Exit if nothing to undo
905 if (_undoStack == null || _undoStack.isEmpty())
907 // Has track got unsaved data?
908 boolean unsaved = hasDataUnsaved();
909 // Confirm operation with dialog
910 int answer = JOptionPane.showConfirmDialog(_frame,
911 I18nManager.getText("dialog.clearundo.text"),
912 I18nManager.getText("dialog.clearundo.title"),
913 JOptionPane.YES_NO_OPTION);
914 if (answer == JOptionPane.YES_OPTION)
917 _lastSavePosition = 0;
918 if (unsaved) _lastSavePosition = -1;
919 UpdateMessageBroker.informSubscribers();
925 * Undo the specified number of actions
926 * @param inNumUndos number of actions to undo
928 public void undoActions(int inNumUndos)
932 for (int i=0; i<inNumUndos; i++)
934 _undoStack.pop().performUndo(_trackInfo);
936 String message = "" + inNumUndos + " "
937 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
938 UpdateMessageBroker.informSubscribers(message);
940 catch (UndoException ue)
942 showErrorMessageNoLookup("error.undofailed.title",
943 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
946 catch (EmptyStackException empty) {}
947 UpdateMessageBroker.informSubscribers();
951 * @return the current data status, used for later comparison
953 public DataStatus getCurrentDataStatus() {
954 return new DataStatus(_undoStack.size(), _undoStack.getNumTimesDeleted());
958 * Show a map url in an external browser
959 * @param inSourceIndex index of map source to use
961 public void showExternalMap(int inSourceIndex)
963 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
967 * Display a standard error message
968 * @param inTitleKey key to lookup for window title
969 * @param inMessageKey key to lookup for error message
971 public void showErrorMessage(String inTitleKey, String inMessageKey)
973 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
974 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
978 * Display a standard error message
979 * @param inTitleKey key to lookup for window title
980 * @param inMessage error message
982 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
984 JOptionPane.showMessageDialog(_frame, inMessage,
985 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
989 * @param inViewport viewport object
991 public void setViewport(Viewport inViewport)
993 _viewport = inViewport;
997 * @return current viewport object
999 public Viewport getViewport()
1005 * Set the controller for the full screen mode
1006 * @param inController controller object
1008 public void setSidebarController(SidebarController inController)
1010 _sidebarController = inController;
1014 * Toggle sidebars on and off
1016 public void toggleSidebars()
1018 _sidebarController.toggle();
1021 /** @return true if App is currently busy with loading data */
1022 public boolean isBusyLoading() {
1023 return _busyLoading;
1026 /** @return current app mode */
1027 public AppMode getCurrentMode() {
1031 /** @param inMode the current app mode */
1032 public void setCurrentMode(AppMode inMode) {