4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
8 import javax.swing.JFrame;
9 import javax.swing.JOptionPane;
11 import tim.prune.config.Config;
12 import tim.prune.data.Checker;
13 import tim.prune.data.DataPoint;
14 import tim.prune.data.Field;
15 import tim.prune.data.LatLonRectangle;
16 import tim.prune.data.NumberUtils;
17 import tim.prune.data.Photo;
18 import tim.prune.data.PhotoList;
19 import tim.prune.data.PointCreateOptions;
20 import tim.prune.data.RecentFile;
21 import tim.prune.data.SourceInfo;
22 import tim.prune.data.Track;
23 import tim.prune.data.TrackInfo;
24 import tim.prune.data.SourceInfo.FILE_TYPE;
25 import tim.prune.data.Unit;
26 import tim.prune.function.AsyncMediaLoader;
27 import tim.prune.function.SelectTracksFunction;
28 import tim.prune.function.edit.FieldEditList;
29 import tim.prune.function.edit.PointEditor;
30 import tim.prune.function.settings.SaveConfig;
31 import tim.prune.gui.MenuManager;
32 import tim.prune.gui.SidebarController;
33 import tim.prune.gui.UndoManager;
34 import tim.prune.gui.Viewport;
35 import tim.prune.gui.colour.ColourerCaretaker;
36 import tim.prune.gui.colour.PointColourer;
37 import tim.prune.load.FileLoader;
38 import tim.prune.load.JpegLoader;
39 import tim.prune.load.MediaLinkInfo;
40 import tim.prune.load.TrackNameList;
41 import tim.prune.save.ExifSaver;
42 import tim.prune.save.FileSaver;
43 import tim.prune.tips.TipManager;
44 import tim.prune.undo.*;
48 * Main controller for the application
53 private JFrame _frame = null;
54 private Track _track = null;
55 private TrackInfo _trackInfo = null;
56 private int _lastSavePosition = 0;
57 private MenuManager _menuManager = null;
58 private SidebarController _sidebarController = null;
59 private FileLoader _fileLoader = null;
60 private JpegLoader _jpegLoader = null;
61 private FileSaver _fileSaver = null;
62 private UndoStack _undoStack = null;
63 private ColourerCaretaker _colCaretaker = null;
64 private boolean _mangleTimestampsConfirmed = false;
65 private Viewport _viewport = null;
66 private ArrayList<File> _dataFiles = null;
67 private boolean _autoAppendNextFile = false;
68 private boolean _busyLoading = false;
69 private AppMode _appMode = AppMode.NORMAL;
71 /** Enum for the app mode - currently only two options but may expand later */
72 public enum AppMode {NORMAL, DRAWRECT};
77 * @param inFrame frame object for application
79 public App(JFrame inFrame)
82 _undoStack = new UndoStack();
84 _trackInfo = new TrackInfo(_track);
85 FunctionLibrary.initialise(this);
86 _colCaretaker = new ColourerCaretaker(this);
87 UpdateMessageBroker.addSubscriber(_colCaretaker);
88 _colCaretaker.setColourer(Config.getPointColourer());
93 * @return the current TrackInfo
95 public TrackInfo getTrackInfo()
101 * @return the dialog frame
103 public JFrame getFrame()
109 * Check if the application has unsaved data
110 * @return true if data is unsaved, false otherwise
112 public boolean hasDataUnsaved()
114 return (_undoStack.size() > _lastSavePosition
115 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
119 * @return the undo stack
121 public UndoStack getUndoStack()
127 * Update the system's point colourer using the one in the Config
129 public void updatePointColourer()
131 if (_colCaretaker != null) {
132 _colCaretaker.setColourer(Config.getPointColourer());
137 * @return colourer object, or null
139 public PointColourer getPointColourer()
141 if (_colCaretaker == null) {return null;}
142 return _colCaretaker.getColourer();
146 * Show the specified tip if appropriate
147 * @param inTipNumber tip number from TipManager
149 public void showTip(int inTipNumber)
151 String key = TipManager.fireTipTrigger(inTipNumber);
152 if (key != null && !key.equals(""))
154 JOptionPane.showMessageDialog(_frame, I18nManager.getText(key),
155 I18nManager.getText("tip.title"), JOptionPane.INFORMATION_MESSAGE);
161 * Load the specified data files one by one
162 * @param inDataFiles arraylist containing File objects to load
164 public void loadDataFiles(ArrayList<File> inDataFiles)
166 if (inDataFiles == null || inDataFiles.size() == 0) {
171 _dataFiles = inDataFiles;
172 File f = _dataFiles.get(0);
173 _dataFiles.remove(0);
174 // Start load of specified file
175 if (_fileLoader == null)
176 _fileLoader = new FileLoader(this, _frame);
177 _autoAppendNextFile = false; // prompt for append
178 _fileLoader.openFile(f);
183 * Complete a function execution
184 * @param inUndo undo object to be added to stack
185 * @param inConfirmText confirmation text
187 public void completeFunction(UndoOperation inUndo, String inConfirmText)
189 _undoStack.add(inUndo);
190 UpdateMessageBroker.informSubscribers(inConfirmText);
191 setCurrentMode(AppMode.NORMAL);
195 * Set the MenuManager object to be informed about changes
196 * @param inManager MenuManager object
198 public void setMenuManager(MenuManager inManager)
200 _menuManager = inManager;
205 * Open a file containing track or waypoint data
207 public void openFile()
209 if (_fileLoader == null)
210 _fileLoader = new FileLoader(this, _frame);
211 _fileLoader.openFile();
216 * Add a photo or a directory of photos
218 public void addPhotos()
220 if (_jpegLoader == null)
221 _jpegLoader = new JpegLoader(this, _frame);
222 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
226 * Save the file in the selected format
228 public void saveFile()
230 if (_track == null) {
231 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
235 if (_fileSaver == null) {
236 _fileSaver = new FileSaver(this, _frame);
239 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
240 _fileSaver.showDialog(delim);
246 * Exit the application if confirmed
252 _frame.requestFocus();
253 // check if ok to exit
254 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
255 if (!hasDataUnsaved()
256 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
257 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
258 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
259 == JOptionPane.YES_OPTION)
262 if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
263 new SaveConfig(this).silentSave();
271 * Edit the currently selected point
273 public void editCurrentPoint()
277 DataPoint currentPoint = _trackInfo.getCurrentPoint();
278 if (currentPoint != null)
280 // Open point dialog to display details
281 PointEditor editor = new PointEditor(this, _frame);
282 editor.showDialog(_track, currentPoint);
289 * Complete the point edit
290 * @param inEditList field values to edit
291 * @param inUndoList field values before edit
293 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
295 DataPoint currentPoint = _trackInfo.getCurrentPoint();
296 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
298 // add information to undo stack
299 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
300 // pass to track for completion
301 if (_track.editPoint(currentPoint, inEditList, false))
303 _undoStack.add(undo);
304 // Confirm point edit
305 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
312 * Delete the currently selected point
314 public void deleteCurrentPoint()
316 if (_track == null) {return;}
317 DataPoint currentPoint = _trackInfo.getCurrentPoint();
318 if (currentPoint != null)
321 boolean deletePhoto = false;
322 Photo currentPhoto = currentPoint.getPhoto();
323 if (currentPhoto != null)
325 // Confirm deletion of photo or decoupling
326 int response = JOptionPane.showConfirmDialog(_frame,
327 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
328 I18nManager.getText("dialog.deletepoint.title"),
329 JOptionPane.YES_NO_CANCEL_OPTION);
330 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
332 // cancel pressed- abort delete
335 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
337 // store necessary information to undo it later
338 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
339 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
340 int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
341 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
342 // Construct Undo object
343 UndoDeletePoint undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
344 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
345 undo.setAtBoundaryOfSelectedRange(pointIndex == _trackInfo.getSelection().getStart() ||
346 pointIndex == _trackInfo.getSelection().getEnd());
347 // call track to delete point
348 if (_trackInfo.deletePoint())
350 // Delete was successful so add undo info to stack
351 _undoStack.add(undo);
352 if (currentPhoto != null)
354 // delete photo if necessary
357 _trackInfo.getPhotoList().deletePhoto(photoIndex);
361 // decouple photo from point
362 currentPhoto.setDataPoint(null);
364 UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
366 // Delete audio object (without bothering to ask)
367 if (audioIndex > -1) {
368 _trackInfo.getAudioList().deleteAudio(audioIndex);
371 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
372 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
379 * Reverse the currently selected section of the track
381 public void reverseRange()
383 // check whether Timestamp field exists, and if so confirm reversal
384 int selStart = _trackInfo.getSelection().getStart();
385 int selEnd = _trackInfo.getSelection().getEnd();
386 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
387 || _mangleTimestampsConfirmed
388 || (JOptionPane.showConfirmDialog(_frame,
389 I18nManager.getText("dialog.confirmreversetrack.text"),
390 I18nManager.getText("dialog.confirmreversetrack.title"),
391 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
393 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
394 // call track to reverse range
395 if (_track.reverseRange(selStart, selEnd))
397 _undoStack.add(undo);
399 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
405 * Complete the add time offset function with the specified offset in seconds
406 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
408 public void finishAddTimeOffsetSeconds(long inTimeOffset)
410 // Construct undo information
411 int selStart = _trackInfo.getSelection().getStart();
412 int selEnd = _trackInfo.getSelection().getEnd();
413 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
414 if (_trackInfo.getTrack().addTimeOffsetSeconds(selStart, selEnd, inTimeOffset, false))
416 _undoStack.add(undo);
417 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
418 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
424 * Complete the add altitude offset function with the specified offset
425 * @param inOffset altitude offset to add as String
426 * @param inUnit altitude units of offset (eg Feet, Metres)
428 public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
431 if (inOffset == null || inOffset.equals("") || inUnit == null) {
434 // Construct undo information
435 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
436 int selStart = _trackInfo.getSelection().getStart();
437 int selEnd = _trackInfo.getSelection().getEnd();
438 // How many decimal places are given in the offset?
439 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
440 boolean success = false;
441 // Decimal offset given
443 double offsetd = Double.parseDouble(inOffset);
444 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
446 catch (NumberFormatException nfe) {}
449 _undoStack.add(undo);
450 _trackInfo.getSelection().markInvalid();
451 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
452 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
458 * Remove altitudes from selected points
460 public void removeAltitudes(int selStart, int selEnd)
462 UndoRemoveAltitudes undo = new UndoRemoveAltitudes(_trackInfo, selStart, selEnd);
463 if (_trackInfo.getTrack().removeAltitudes(selStart, selEnd))
465 _undoStack.add(undo);
466 _trackInfo.getSelection().markInvalid();
467 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
468 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.removealtitudes"));
474 * Merge the track segments within the current selection
476 public void mergeTrackSegments()
478 if (_trackInfo.getSelection().hasRangeSelected())
480 // Maybe could check segment start flags to see if it's worth merging
481 // If first track point is already start and no other seg starts then do nothing
483 int selStart = _trackInfo.getSelection().getStart();
484 int selEnd = _trackInfo.getSelection().getEnd();
486 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
487 // Call track to merge segments
488 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
489 _undoStack.add(undo);
490 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
497 * Average the selected points
499 public void averageSelection()
501 // Find following track point
502 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
503 boolean segFlag = false;
504 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
505 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
506 // call track info object to do the averaging
507 if (_trackInfo.average())
509 _undoStack.add(undo);
515 * Create a new point at the end of the track
516 * @param inPoint point to add
518 public void createPoint(DataPoint inPoint)
520 createPoint(inPoint, true);
524 * Create a new point at the end of the track
525 * @param inPoint point to add
526 * @param inNewSegment true for a single point, false for a continuation
528 public void createPoint(DataPoint inPoint, boolean inNewSegment)
530 // create undo object
531 UndoCreatePoint undo = new UndoCreatePoint();
532 _undoStack.add(undo);
533 // add point to track
534 inPoint.setSegmentStart(inNewSegment);
535 _track.appendPoints(new DataPoint[] {inPoint});
536 // ensure track's field list contains point's fields
537 _track.extendFieldList(inPoint.getFieldList());
538 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
540 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
545 * Create a new point before the given position
546 * @param inPoint point to add
547 * @param inIndex index of following point
549 public void createPoint(DataPoint inPoint, int inIndex)
551 // create undo object
552 UndoInsert undo = new UndoInsert(inIndex, 1);
553 _undoStack.add(undo);
554 // add point to track
555 _track.insertPoint(inPoint, inIndex);
556 // ensure track's field list contains point's fields
557 _track.extendFieldList(inPoint.getFieldList());
558 _trackInfo.selectPoint(inIndex);
559 final int selStart = _trackInfo.getSelection().getStart();
560 final int selEnd = _trackInfo.getSelection().getEnd();
561 if (selStart < inIndex && selEnd >= inIndex)
563 // Extend end of selection by 1
564 _trackInfo.getSelection().selectRange(selStart, selEnd+1);
567 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
572 * Cut the current selection and move it to before the currently selected point
574 public void cutAndMoveSelection()
576 int startIndex = _trackInfo.getSelection().getStart();
577 int endIndex = _trackInfo.getSelection().getEnd();
578 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
579 // If timestamps would be mangled by cut/move, confirm
580 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
581 || _mangleTimestampsConfirmed
582 || (JOptionPane.showConfirmDialog(_frame,
583 I18nManager.getText("dialog.confirmcutandmove.text"),
584 I18nManager.getText("dialog.confirmcutandmove.title"),
585 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
587 // Find points to set segment flags
588 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
589 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
590 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
592 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
593 // Call track info to move track section
594 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
596 // Set segment start flags (first track point, next track point, move to point)
597 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
598 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
599 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
601 // Add undo object to stack, set confirm message
602 _undoStack.add(undo);
603 _trackInfo.getSelection().selectRange(-1, -1);
604 UpdateMessageBroker.informSubscribers();
605 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
613 public void selectNone()
615 // deselect point, range and photo
616 _trackInfo.getSelection().clearAll();
617 _track.clearDeletionMarkers();
621 * Receive loaded data and determine whether to filter on tracks or not
622 * @param inFieldArray array of fields
623 * @param inDataArray array of data
624 * @param inSourceInfo information about the source of the data
625 * @param inTrackNameList information about the track names
627 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
628 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
630 // no link array given
631 informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
632 inTrackNameList, null);
636 * Receive loaded data and determine whether to filter on tracks or not
637 * @param inFieldArray array of fields
638 * @param inDataArray array of data
639 * @param inOptions creation options such as units
640 * @param inSourceInfo information about the source of the data
641 * @param inTrackNameList information about the track names
643 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
644 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
646 // no link array given
647 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
648 inTrackNameList, null);
652 * Receive loaded data and determine whether to filter on tracks or not
653 * @param inFieldArray array of fields
654 * @param inDataArray array of data
655 * @param inOptions creation options such as units
656 * @param inSourceInfo information about the source of the data
657 * @param inTrackNameList information about the track names
658 * @param inLinkInfo links to photo/audio clips
660 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
661 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
663 // Check whether loaded array can be properly parsed into a Track
664 Track loadedTrack = new Track();
665 loadedTrack.load(inFieldArray, inDataArray, inOptions);
666 if (loadedTrack.getNumPoints() <= 0)
668 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
669 // load next file if there's a queue
673 // Check for doubled track
674 if (Checker.isDoubledTrack(loadedTrack)) {
675 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
676 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
680 // Attach photos and/or audio clips to points
681 if (inLinkInfo != null)
683 String[] linkArray = inLinkInfo.getLinkArray();
684 if (linkArray != null) {
685 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
688 // Look at TrackNameList, decide whether to filter or not
689 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
691 // Launch a dialog to let the user choose which tracks to load, then continue
692 new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
695 // go directly to load
696 informDataLoaded(loadedTrack, inSourceInfo);
698 setCurrentMode(AppMode.NORMAL);
703 * Receive loaded data and optionally merge with current Track
704 * @param inLoadedTrack loaded track
705 * @param inSourceInfo information about the source of the data
707 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
709 // Decide whether to load or append
710 if (_track.getNumPoints() > 0)
712 // ask whether to replace or append
714 if (_autoAppendNextFile) {
715 // Automatically append the next file
716 answer = JOptionPane.YES_OPTION;
719 // Ask whether to append or not
720 answer = JOptionPane.showConfirmDialog(_frame,
721 I18nManager.getText("dialog.openappend.text"),
722 I18nManager.getText("dialog.openappend.title"),
723 JOptionPane.YES_NO_CANCEL_OPTION);
725 _autoAppendNextFile = false; // reset flag to cancel autoappend
727 if (answer == JOptionPane.YES_OPTION)
729 // append data to current Track
730 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
731 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
732 _undoStack.add(undo);
733 _track.combine(inLoadedTrack);
734 // set source information
735 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
736 _trackInfo.getFileInfo().addSource(inSourceInfo);
738 else if (answer == JOptionPane.NO_OPTION)
740 // Don't append, replace data
741 PhotoList photos = null;
742 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
743 photos = _trackInfo.getPhotoList().cloneList();
745 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
746 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
747 _undoStack.add(undo);
748 _lastSavePosition = _undoStack.size();
749 _trackInfo.getSelection().clearAll();
750 _track.load(inLoadedTrack);
751 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
752 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
753 _trackInfo.getPhotoList().removeCorrelatedPhotos();
754 _trackInfo.getAudioList().removeCorrelatedAudios();
759 // Currently no data held, so transfer received data
760 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
761 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
762 _undoStack.add(undo);
763 _lastSavePosition = _undoStack.size();
764 _trackInfo.getSelection().clearAll();
765 _track.load(inLoadedTrack);
766 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
767 _trackInfo.getFileInfo().addSource(inSourceInfo);
769 // Update config before subscribers are told
770 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
771 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
772 UpdateMessageBroker.informSubscribers();
774 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
775 + " '" + inSourceInfo.getName() + "'");
777 _menuManager.informFileLoaded();
778 // recentre viewport on new file data
779 _viewport.recentreViewport();
781 _busyLoading = false;
782 // load next file if there's a queue
787 * Inform the app that NO data was loaded, eg cancel pressed
788 * Only needed if there's another file waiting in the queue
790 public void informNoDataLoaded()
792 // Load next file if there's a queue
797 * External trigger to automatically append the next loaded file
798 * instead of prompting to replace or append
800 public void autoAppendNextFile()
802 _autoAppendNextFile = true;
806 * Load the next file in the waiting list, if any
808 private void loadNextFile()
810 if (_dataFiles == null || _dataFiles.size() == 0) {
814 new Thread(new Runnable() {
816 File f = _dataFiles.get(0);
817 _dataFiles.remove(0);
818 _autoAppendNextFile = true;
819 _fileLoader.openFile(f);
827 * Accept a list of loaded photos
828 * @param inPhotoSet Set of Photo objects
830 public void informPhotosLoaded(Set<Photo> inPhotoSet)
832 if (inPhotoSet != null && !inPhotoSet.isEmpty())
834 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
835 int numPhotosAdded = numsAdded[0];
836 int numPointsAdded = numsAdded[1];
837 if (numPhotosAdded > 0)
839 // Save numbers so load can be undone
840 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
842 if (numPhotosAdded == 1) {
843 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
846 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
848 // MAYBE: Improve message when photo(s) fail to load (eg already added)
849 UpdateMessageBroker.informSubscribers();
851 if (numPointsAdded > 0) _menuManager.informFileLoaded();
857 * Save the coordinates of photos in their exif data
859 public void saveExif()
861 ExifSaver saver = new ExifSaver(_frame);
862 saver.saveExifInformation(_trackInfo.getPhotoList());
867 * Inform the app that the data has been saved
869 public void informDataSaved()
871 _lastSavePosition = _undoStack.size();
878 public void beginUndo()
880 if (_undoStack.isEmpty())
883 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
884 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
888 new UndoManager(this, _frame).show();
894 * Clear the undo stack (losing all undo information
896 public void clearUndo()
898 // Exit if nothing to undo
899 if (_undoStack == null || _undoStack.isEmpty())
901 // Has track got unsaved data?
902 boolean unsaved = hasDataUnsaved();
903 // Confirm operation with dialog
904 int answer = JOptionPane.showConfirmDialog(_frame,
905 I18nManager.getText("dialog.clearundo.text"),
906 I18nManager.getText("dialog.clearundo.title"),
907 JOptionPane.YES_NO_OPTION);
908 if (answer == JOptionPane.YES_OPTION)
911 _lastSavePosition = 0;
912 if (unsaved) _lastSavePosition = -1;
913 UpdateMessageBroker.informSubscribers();
919 * Undo the specified number of actions
920 * @param inNumUndos number of actions to undo
922 public void undoActions(int inNumUndos)
926 for (int i=0; i<inNumUndos; i++)
928 _undoStack.popOperation().performUndo(_trackInfo);
930 String message = "" + inNumUndos + " "
931 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
932 UpdateMessageBroker.informSubscribers(message);
934 catch (UndoException ue)
936 showErrorMessageNoLookup("error.undofailed.title",
937 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
940 catch (EmptyStackException empty) {}
941 UpdateMessageBroker.informSubscribers();
945 * @return the current data status, used for later comparison
947 public DataStatus getCurrentDataStatus()
949 return new DataStatus(_undoStack.size(), _undoStack.getNumUndos());
954 * Display a standard error message
955 * @param inTitleKey key to lookup for window title
956 * @param inMessageKey key to lookup for error message
958 public void showErrorMessage(String inTitleKey, String inMessageKey)
960 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
961 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
965 * Display a standard error message
966 * @param inTitleKey key to lookup for window title
967 * @param inMessage error message
969 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
971 JOptionPane.showMessageDialog(_frame, inMessage,
972 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
976 * @param inViewport viewport object
978 public void setViewport(Viewport inViewport)
980 _viewport = inViewport;
984 * @return current viewport object
986 public Viewport getViewport()
992 * Set the controller for the full screen mode
993 * @param inController controller object
995 public void setSidebarController(SidebarController inController)
997 _sidebarController = inController;
1001 * Toggle sidebars on and off
1003 public void toggleSidebars()
1005 _sidebarController.toggle();
1008 /** @return true if App is currently busy with loading data */
1009 public boolean isBusyLoading() {
1010 return _busyLoading;
1013 /** @return current app mode */
1014 public AppMode getCurrentMode() {
1018 /** @param inMode the current app mode */
1019 public void setCurrentMode(AppMode inMode) {