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.load.FileLoader;
39 import tim.prune.load.JpegLoader;
40 import tim.prune.load.MediaLinkInfo;
41 import tim.prune.load.TrackNameList;
42 import tim.prune.save.ExifSaver;
43 import tim.prune.save.FileSaver;
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 Stack<UndoOperation> _undoStack = null;
63 private boolean _mangleTimestampsConfirmed = false;
64 private Viewport _viewport = null;
65 private ArrayList<File> _dataFiles = null;
66 private boolean _autoAppendNextFile = false;
67 private boolean _busyLoading = false;
68 private AppMode _appMode = AppMode.NORMAL;
70 /** Enum for the app mode - currently only two options but may expand later */
71 public enum AppMode {NORMAL, DRAWRECT};
76 * @param inFrame frame object for application
78 public App(JFrame inFrame)
81 _undoStack = new Stack<UndoOperation>();
83 _trackInfo = new TrackInfo(_track);
84 FunctionLibrary.initialise(this);
89 * @return the current TrackInfo
91 public TrackInfo getTrackInfo()
97 * @return the dialog frame
99 public JFrame getFrame()
105 * Check if the application has unsaved data
106 * @return true if data is unsaved, false otherwise
108 public boolean hasDataUnsaved()
110 return (_undoStack.size() > _lastSavePosition
111 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
115 * @return the undo stack
117 public Stack<UndoOperation> getUndoStack()
123 * Load the specified data files one by one
124 * @param inDataFiles arraylist containing File objects to load
126 public void loadDataFiles(ArrayList<File> inDataFiles)
128 if (inDataFiles == null || inDataFiles.size() == 0) {
133 _dataFiles = inDataFiles;
134 File f = _dataFiles.get(0);
135 _dataFiles.remove(0);
136 // Start load of specified file
137 if (_fileLoader == null)
138 _fileLoader = new FileLoader(this, _frame);
139 _autoAppendNextFile = false; // prompt for append
140 _fileLoader.openFile(f);
145 * Complete a function execution
146 * @param inUndo undo object to be added to stack
147 * @param inConfirmText confirmation text
149 public void completeFunction(UndoOperation inUndo, String inConfirmText)
151 _undoStack.add(inUndo);
152 UpdateMessageBroker.informSubscribers(inConfirmText);
153 setCurrentMode(AppMode.NORMAL);
157 * Set the MenuManager object to be informed about changes
158 * @param inManager MenuManager object
160 public void setMenuManager(MenuManager inManager)
162 _menuManager = inManager;
167 * Open a file containing track or waypoint data
169 public void openFile()
171 if (_fileLoader == null)
172 _fileLoader = new FileLoader(this, _frame);
173 _fileLoader.openFile();
178 * Add a photo or a directory of photos
180 public void addPhotos()
182 if (_jpegLoader == null)
183 _jpegLoader = new JpegLoader(this, _frame);
184 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
188 * Save the file in the selected format
190 public void saveFile()
192 if (_track == null) {
193 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
197 if (_fileSaver == null) {
198 _fileSaver = new FileSaver(this, _frame);
201 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
202 _fileSaver.showDialog(delim);
208 * Exit the application if confirmed
214 _frame.requestFocus();
215 // check if ok to exit
216 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
217 if (!hasDataUnsaved()
218 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
219 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
220 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
221 == JOptionPane.YES_OPTION)
224 if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
225 new SaveConfig(this).silentSave();
233 * Edit the currently selected point
235 public void editCurrentPoint()
239 DataPoint currentPoint = _trackInfo.getCurrentPoint();
240 if (currentPoint != null)
242 // Open point dialog to display details
243 PointEditor editor = new PointEditor(this, _frame);
244 editor.showDialog(_track, currentPoint);
251 * Complete the point edit
252 * @param inEditList field values to edit
253 * @param inUndoList field values before edit
255 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
257 DataPoint currentPoint = _trackInfo.getCurrentPoint();
258 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
260 // add information to undo stack
261 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
262 // pass to track for completion
263 if (_track.editPoint(currentPoint, inEditList, false))
265 _undoStack.push(undo);
266 // Confirm point edit
267 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
274 * Delete the currently selected point
276 public void deleteCurrentPoint()
278 if (_track == null) {return;}
279 DataPoint currentPoint = _trackInfo.getCurrentPoint();
280 if (currentPoint != null)
283 boolean deletePhoto = false;
284 Photo currentPhoto = currentPoint.getPhoto();
285 if (currentPhoto != null)
287 // Confirm deletion of photo or decoupling
288 int response = JOptionPane.showConfirmDialog(_frame,
289 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
290 I18nManager.getText("dialog.deletepoint.title"),
291 JOptionPane.YES_NO_CANCEL_OPTION);
292 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
294 // cancel pressed- abort delete
297 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
299 // store necessary information to undo it later
300 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
301 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
302 int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
303 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
304 // Construct Undo object
305 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
306 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
307 // call track to delete point
308 if (_trackInfo.deletePoint())
310 // Delete was successful so add undo info to stack
311 _undoStack.push(undo);
312 if (currentPhoto != null)
314 // delete photo if necessary
317 _trackInfo.getPhotoList().deletePhoto(photoIndex);
321 // decouple photo from point
322 currentPhoto.setDataPoint(null);
324 UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
326 // Delete audio object (without bothering to ask)
327 if (audioIndex > -1) {
328 _trackInfo.getAudioList().deleteAudio(audioIndex);
331 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
332 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
339 * Finish the compression by deleting the marked points
341 public void finishCompressTrack()
343 UndoDeleteMarked undo = new UndoDeleteMarked(_track);
344 // call track to do compress
345 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
346 // add to undo stack if successful
347 if (numPointsDeleted > 0)
349 undo.setNumPointsDeleted(numPointsDeleted);
350 _undoStack.add(undo);
351 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
352 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
355 showErrorMessage("function.compress", "dialog.deletemarked.nonefound");
360 * Reverse the currently selected section of the track
362 public void reverseRange()
364 // check whether Timestamp field exists, and if so confirm reversal
365 int selStart = _trackInfo.getSelection().getStart();
366 int selEnd = _trackInfo.getSelection().getEnd();
367 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
368 || _mangleTimestampsConfirmed
369 || (JOptionPane.showConfirmDialog(_frame,
370 I18nManager.getText("dialog.confirmreversetrack.text"),
371 I18nManager.getText("dialog.confirmreversetrack.title"),
372 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
374 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
375 // call track to reverse range
376 if (_track.reverseRange(selStart, selEnd))
378 _undoStack.add(undo);
380 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
386 * Complete the add time offset function with the specified offset
387 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
389 public void finishAddTimeOffset(long inTimeOffset)
391 // Construct undo information
392 int selStart = _trackInfo.getSelection().getStart();
393 int selEnd = _trackInfo.getSelection().getEnd();
394 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
395 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
397 _undoStack.add(undo);
398 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
399 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
405 * Complete the add altitude offset function with the specified offset
406 * @param inOffset altitude offset to add as String
407 * @param inUnit altitude units of offset (eg Feet, Metres)
409 public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
412 if (inOffset == null || inOffset.equals("") || inUnit == null) {
415 // Construct undo information
416 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
417 int selStart = _trackInfo.getSelection().getStart();
418 int selEnd = _trackInfo.getSelection().getEnd();
419 // How many decimal places are given in the offset?
420 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
421 boolean success = false;
422 // Decimal offset given
424 double offsetd = Double.parseDouble(inOffset);
425 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
427 catch (NumberFormatException nfe) {}
430 _undoStack.add(undo);
431 _trackInfo.getSelection().markInvalid();
432 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
433 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
439 * Merge the track segments within the current selection
441 public void mergeTrackSegments()
443 if (_trackInfo.getSelection().hasRangeSelected())
445 // Maybe could check segment start flags to see if it's worth merging
446 // If first track point is already start and no other seg starts then do nothing
448 int selStart = _trackInfo.getSelection().getStart();
449 int selEnd = _trackInfo.getSelection().getEnd();
451 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
452 // Call track to merge segments
453 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
454 _undoStack.add(undo);
455 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
462 * Average the selected points
464 public void averageSelection()
466 // Find following track point
467 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
468 boolean segFlag = false;
469 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
470 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
471 // call track info object to do the averaging
472 if (_trackInfo.average())
474 _undoStack.add(undo);
480 * Create a new point at the end of the track
481 * @param inPoint point to add
483 public void createPoint(DataPoint inPoint)
485 // create undo object
486 UndoCreatePoint undo = new UndoCreatePoint();
487 _undoStack.add(undo);
488 // add point to track
489 inPoint.setSegmentStart(true);
490 _track.appendPoints(new DataPoint[] {inPoint});
491 // ensure track's field list contains point's fields
492 _track.extendFieldList(inPoint.getFieldList());
493 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
495 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
500 * Create a new point before the given position
501 * @param inPoint point to add
502 * @param inIndex index of following point
504 public void createPoint(DataPoint inPoint, int inIndex)
506 // create undo object
507 UndoInsert undo = new UndoInsert(inIndex, 1);
508 _undoStack.add(undo);
509 // add point to track
510 _track.insertPoint(inPoint, inIndex);
511 // ensure track's field list contains point's fields
512 _track.extendFieldList(inPoint.getFieldList());
513 _trackInfo.selectPoint(inIndex);
514 final int selStart = _trackInfo.getSelection().getStart();
515 final int selEnd = _trackInfo.getSelection().getEnd();
516 if (selStart < inIndex && selEnd > inIndex)
518 // Extend end of selection by 1
519 _trackInfo.getSelection().selectRange(selStart, selEnd+1);
522 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
527 * Cut the current selection and move it to before the currently selected point
529 public void cutAndMoveSelection()
531 int startIndex = _trackInfo.getSelection().getStart();
532 int endIndex = _trackInfo.getSelection().getEnd();
533 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
534 // If timestamps would be mangled by cut/move, confirm
535 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
536 || _mangleTimestampsConfirmed
537 || (JOptionPane.showConfirmDialog(_frame,
538 I18nManager.getText("dialog.confirmcutandmove.text"),
539 I18nManager.getText("dialog.confirmcutandmove.title"),
540 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
542 // Find points to set segment flags
543 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
544 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
545 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
547 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
548 // Call track info to move track section
549 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
551 // Set segment start flags (first track point, next track point, move to point)
552 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
553 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
554 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
556 // Add undo object to stack, set confirm message
557 _undoStack.add(undo);
558 _trackInfo.getSelection().selectRange(-1, -1);
559 UpdateMessageBroker.informSubscribers();
560 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
568 public void selectNone()
570 // deselect point, range and photo
571 _trackInfo.getSelection().clearAll();
572 _track.clearDeletionMarkers();
576 * Receive loaded data and determine whether to filter on tracks or not
577 * @param inFieldArray array of fields
578 * @param inDataArray array of data
579 * @param inSourceInfo information about the source of the data
580 * @param inTrackNameList information about the track names
582 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
583 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
585 // no link array given
586 informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
587 inTrackNameList, null);
591 * Receive loaded data and determine whether to filter on tracks or not
592 * @param inFieldArray array of fields
593 * @param inDataArray array of data
594 * @param inOptions creation options such as units
595 * @param inSourceInfo information about the source of the data
596 * @param inTrackNameList information about the track names
598 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
599 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
601 // no link array given
602 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
603 inTrackNameList, null);
607 * Receive loaded data and determine whether to filter on tracks or not
608 * @param inFieldArray array of fields
609 * @param inDataArray array of data
610 * @param inOptions creation options such as units
611 * @param inSourceInfo information about the source of the data
612 * @param inTrackNameList information about the track names
613 * @param inLinkInfo links to photo/audio clips
615 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
616 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
618 // Check whether loaded array can be properly parsed into a Track
619 Track loadedTrack = new Track();
620 loadedTrack.load(inFieldArray, inDataArray, inOptions);
621 if (loadedTrack.getNumPoints() <= 0)
623 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
624 // load next file if there's a queue
628 // Check for doubled track
629 if (Checker.isDoubledTrack(loadedTrack)) {
630 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
631 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
635 // Attach photos and/or audio clips to points
636 if (inLinkInfo != null)
638 String[] linkArray = inLinkInfo.getLinkArray();
639 if (linkArray != null) {
640 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
643 // Look at TrackNameList, decide whether to filter or not
644 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
646 // Launch a dialog to let the user choose which tracks to load, then continue
647 new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
650 // go directly to load
651 informDataLoaded(loadedTrack, inSourceInfo);
653 setCurrentMode(AppMode.NORMAL);
658 * Receive loaded data and optionally merge with current Track
659 * @param inLoadedTrack loaded track
660 * @param inSourceInfo information about the source of the data
662 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
664 // Decide whether to load or append
665 if (_track.getNumPoints() > 0)
667 // ask whether to replace or append
669 if (_autoAppendNextFile) {
670 // Automatically append the next file
671 answer = JOptionPane.YES_OPTION;
674 // Ask whether to append or not
675 answer = JOptionPane.showConfirmDialog(_frame,
676 I18nManager.getText("dialog.openappend.text"),
677 I18nManager.getText("dialog.openappend.title"),
678 JOptionPane.YES_NO_CANCEL_OPTION);
680 _autoAppendNextFile = false; // reset flag to cancel autoappend
682 if (answer == JOptionPane.YES_OPTION)
684 // append data to current Track
685 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
686 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
687 _undoStack.add(undo);
688 _track.combine(inLoadedTrack);
689 // set source information
690 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
691 _trackInfo.getFileInfo().addSource(inSourceInfo);
693 else if (answer == JOptionPane.NO_OPTION)
695 // Don't append, replace data
696 PhotoList photos = null;
697 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
698 photos = _trackInfo.getPhotoList().cloneList();
700 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
701 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
702 _undoStack.add(undo);
703 _lastSavePosition = _undoStack.size();
704 _trackInfo.getSelection().clearAll();
705 _track.load(inLoadedTrack);
706 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
707 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
708 _trackInfo.getPhotoList().removeCorrelatedPhotos();
709 _trackInfo.getAudioList().removeCorrelatedAudios();
714 // Currently no data held, so transfer received data
715 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
716 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
717 _undoStack.add(undo);
718 _lastSavePosition = _undoStack.size();
719 _trackInfo.getSelection().clearAll();
720 _track.load(inLoadedTrack);
721 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
722 _trackInfo.getFileInfo().addSource(inSourceInfo);
724 // Update config before subscribers are told
725 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
726 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
727 UpdateMessageBroker.informSubscribers();
729 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
730 + " '" + inSourceInfo.getName() + "'");
732 _menuManager.informFileLoaded();
734 _busyLoading = false;
735 // load next file if there's a queue
740 * Inform the app that NO data was loaded, eg cancel pressed
741 * Only needed if there's another file waiting in the queue
743 public void informNoDataLoaded()
745 // Load next file if there's a queue
750 * External trigger to automatically append the next loaded file
751 * instead of prompting to replace or append
753 public void autoAppendNextFile()
755 _autoAppendNextFile = true;
759 * Load the next file in the waiting list, if any
761 private void loadNextFile()
763 if (_dataFiles == null || _dataFiles.size() == 0) {
767 new Thread(new Runnable() {
769 File f = _dataFiles.get(0);
770 _dataFiles.remove(0);
771 _autoAppendNextFile = true;
772 _fileLoader.openFile(f);
780 * Accept a list of loaded photos
781 * @param inPhotoSet Set of Photo objects
783 public void informPhotosLoaded(Set<Photo> inPhotoSet)
785 if (inPhotoSet != null && !inPhotoSet.isEmpty())
787 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
788 int numPhotosAdded = numsAdded[0];
789 int numPointsAdded = numsAdded[1];
790 if (numPhotosAdded > 0)
792 // Save numbers so load can be undone
793 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
795 if (numPhotosAdded == 1) {
796 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
799 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
801 // MAYBE: Improve message when photo(s) fail to load (eg already added)
802 UpdateMessageBroker.informSubscribers();
804 if (numPointsAdded > 0) _menuManager.informFileLoaded();
810 * Save the coordinates of photos in their exif data
812 public void saveExif()
814 ExifSaver saver = new ExifSaver(_frame);
815 saver.saveExifInformation(_trackInfo.getPhotoList());
820 * Inform the app that the data has been saved
822 public void informDataSaved()
824 _lastSavePosition = _undoStack.size();
831 public void beginUndo()
833 if (_undoStack.isEmpty())
836 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
837 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
841 new UndoManager(this, _frame);
847 * Clear the undo stack (losing all undo information
849 public void clearUndo()
851 // Exit if nothing to undo
852 if (_undoStack == null || _undoStack.isEmpty())
854 // Has track got unsaved data?
855 boolean unsaved = hasDataUnsaved();
856 // Confirm operation with dialog
857 int answer = JOptionPane.showConfirmDialog(_frame,
858 I18nManager.getText("dialog.clearundo.text"),
859 I18nManager.getText("dialog.clearundo.title"),
860 JOptionPane.YES_NO_OPTION);
861 if (answer == JOptionPane.YES_OPTION)
864 _lastSavePosition = 0;
865 if (unsaved) _lastSavePosition = -1;
866 UpdateMessageBroker.informSubscribers();
872 * Undo the specified number of actions
873 * @param inNumUndos number of actions to undo
875 public void undoActions(int inNumUndos)
879 for (int i=0; i<inNumUndos; i++)
881 _undoStack.pop().performUndo(_trackInfo);
883 String message = "" + inNumUndos + " "
884 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
885 UpdateMessageBroker.informSubscribers(message);
887 catch (UndoException ue)
889 showErrorMessageNoLookup("error.undofailed.title",
890 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
893 catch (EmptyStackException empty) {}
894 UpdateMessageBroker.informSubscribers();
899 * Show a map url in an external browser
900 * @param inSourceIndex index of map source to use
902 public void showExternalMap(int inSourceIndex)
904 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
908 * Display a standard error message
909 * @param inTitleKey key to lookup for window title
910 * @param inMessageKey key to lookup for error message
912 public void showErrorMessage(String inTitleKey, String inMessageKey)
914 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
915 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
919 * Display a standard error message
920 * @param inTitleKey key to lookup for window title
921 * @param inMessage error message
923 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
925 JOptionPane.showMessageDialog(_frame, inMessage,
926 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
930 * @param inViewport viewport object
932 public void setViewport(Viewport inViewport)
934 _viewport = inViewport;
938 * @return current viewport object
940 public Viewport getViewport()
946 * Set the controller for the full screen mode
947 * @param inController controller object
949 public void setSidebarController(SidebarController inController)
951 _sidebarController = inController;
955 * Toggle sidebars on and off
957 public void toggleSidebars()
959 _sidebarController.toggle();
962 /** @return true if App is currently busy with loading data */
963 public boolean isBusyLoading() {
967 /** @return current app mode */
968 public AppMode getCurrentMode() {
972 /** @param inMode the current app mode */
973 public void setCurrentMode(AppMode inMode) {