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.Altitude;
14 import tim.prune.data.Checker;
15 import tim.prune.data.DataPoint;
16 import tim.prune.data.Field;
17 import tim.prune.data.LatLonRectangle;
18 import tim.prune.data.NumberUtils;
19 import tim.prune.data.Photo;
20 import tim.prune.data.PhotoList;
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.function.AsyncMediaLoader;
27 import tim.prune.function.SaveConfig;
28 import tim.prune.function.SelectTracksFunction;
29 import tim.prune.function.browser.BrowserLauncher;
30 import tim.prune.function.browser.UrlGenerator;
31 import tim.prune.function.edit.FieldEditList;
32 import tim.prune.function.edit.PointEditor;
33 import tim.prune.gui.MenuManager;
34 import tim.prune.gui.SidebarController;
35 import tim.prune.gui.UndoManager;
36 import tim.prune.gui.Viewport;
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.undo.*;
47 * Main controller for the application
52 private JFrame _frame = null;
53 private Track _track = null;
54 private TrackInfo _trackInfo = null;
55 private int _lastSavePosition = 0;
56 private MenuManager _menuManager = null;
57 private SidebarController _sidebarController = null;
58 private FileLoader _fileLoader = null;
59 private JpegLoader _jpegLoader = null;
60 private FileSaver _fileSaver = null;
61 private Stack<UndoOperation> _undoStack = null;
62 private boolean _mangleTimestampsConfirmed = false;
63 private Viewport _viewport = null;
64 private ArrayList<File> _dataFiles = null;
65 private boolean _autoAppendNextFile = false;
66 private boolean _busyLoading = false;
67 private AppMode _appMode = AppMode.NORMAL;
69 /** Enum for the app mode - currently only two options but may expand later */
70 public enum AppMode {NORMAL, DRAWRECT};
75 * @param inFrame frame object for application
77 public App(JFrame inFrame)
80 _undoStack = new Stack<UndoOperation>();
82 _trackInfo = new TrackInfo(_track);
83 FunctionLibrary.initialise(this);
88 * @return the current TrackInfo
90 public TrackInfo getTrackInfo()
96 * @return the dialog frame
98 public JFrame getFrame()
104 * Check if the application has unsaved data
105 * @return true if data is unsaved, false otherwise
107 public boolean hasDataUnsaved()
109 return (_undoStack.size() > _lastSavePosition
110 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
114 * @return the undo stack
116 public Stack<UndoOperation> getUndoStack()
122 * Load the specified data files one by one
123 * @param inDataFiles arraylist containing File objects to load
125 public void loadDataFiles(ArrayList<File> inDataFiles)
127 if (inDataFiles == null || inDataFiles.size() == 0) {
132 _dataFiles = inDataFiles;
133 File f = _dataFiles.get(0);
134 _dataFiles.remove(0);
135 // Start load of specified file
136 if (_fileLoader == null)
137 _fileLoader = new FileLoader(this, _frame);
138 _autoAppendNextFile = false; // prompt for append
139 _fileLoader.openFile(f);
144 * Complete a function execution
145 * @param inUndo undo object to be added to stack
146 * @param inConfirmText confirmation text
148 public void completeFunction(UndoOperation inUndo, String inConfirmText)
150 _undoStack.add(inUndo);
151 UpdateMessageBroker.informSubscribers(inConfirmText);
152 setCurrentMode(AppMode.NORMAL);
156 * Set the MenuManager object to be informed about changes
157 * @param inManager MenuManager object
159 public void setMenuManager(MenuManager inManager)
161 _menuManager = inManager;
166 * Open a file containing track or waypoint data
168 public void openFile()
170 if (_fileLoader == null)
171 _fileLoader = new FileLoader(this, _frame);
172 _fileLoader.openFile();
177 * Add a photo or a directory of photos
179 public void addPhotos()
181 if (_jpegLoader == null)
182 _jpegLoader = new JpegLoader(this, _frame);
183 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
187 * Save the file in the selected format
189 public void saveFile()
191 if (_track == null) {
192 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
196 if (_fileSaver == null) {
197 _fileSaver = new FileSaver(this, _frame);
200 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
201 _fileSaver.showDialog(delim);
207 * Exit the application if confirmed
213 _frame.requestFocus();
214 // check if ok to exit
215 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
216 if (!hasDataUnsaved()
217 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
218 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
219 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
220 == JOptionPane.YES_OPTION)
223 if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
224 new SaveConfig(this).silentSave();
232 * Edit the currently selected point
234 public void editCurrentPoint()
238 DataPoint currentPoint = _trackInfo.getCurrentPoint();
239 if (currentPoint != null)
241 // Open point dialog to display details
242 PointEditor editor = new PointEditor(this, _frame);
243 editor.showDialog(_track, currentPoint);
250 * Complete the point edit
251 * @param inEditList field values to edit
252 * @param inUndoList field values before edit
254 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
256 DataPoint currentPoint = _trackInfo.getCurrentPoint();
257 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
259 // add information to undo stack
260 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
261 // pass to track for completion
262 if (_track.editPoint(currentPoint, inEditList, false))
264 _undoStack.push(undo);
265 // Confirm point edit
266 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
273 * Delete the currently selected point
275 public void deleteCurrentPoint()
277 if (_track == null) {return;}
278 DataPoint currentPoint = _trackInfo.getCurrentPoint();
279 if (currentPoint != null)
282 boolean deletePhoto = false;
283 Photo currentPhoto = currentPoint.getPhoto();
284 if (currentPhoto != null)
286 // Confirm deletion of photo or decoupling
287 int response = JOptionPane.showConfirmDialog(_frame,
288 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
289 I18nManager.getText("dialog.deletepoint.title"),
290 JOptionPane.YES_NO_CANCEL_OPTION);
291 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
293 // cancel pressed- abort delete
296 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
298 // store necessary information to undo it later
299 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
300 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
301 int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
302 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
303 // Construct Undo object
304 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
305 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
306 // call track to delete point
307 if (_trackInfo.deletePoint())
309 // Delete was successful so add undo info to stack
310 _undoStack.push(undo);
311 if (currentPhoto != null)
313 // delete photo if necessary
316 _trackInfo.getPhotoList().deletePhoto(photoIndex);
320 // decouple photo from point
321 currentPhoto.setDataPoint(null);
323 UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
325 // Delete audio object (without bothering to ask)
326 if (audioIndex > -1) {
327 _trackInfo.getAudioList().deleteAudio(audioIndex);
330 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
331 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
338 * Finish the compression by deleting the marked points
340 public void finishCompressTrack()
342 UndoDeleteMarked undo = new UndoDeleteMarked(_track);
343 // call track to do compress
344 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
345 // add to undo stack if successful
346 if (numPointsDeleted > 0)
348 undo.setNumPointsDeleted(numPointsDeleted);
349 _undoStack.add(undo);
350 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
351 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
354 showErrorMessage("function.compress", "dialog.deletemarked.nonefound");
359 * Reverse the currently selected section of the track
361 public void reverseRange()
363 // check whether Timestamp field exists, and if so confirm reversal
364 int selStart = _trackInfo.getSelection().getStart();
365 int selEnd = _trackInfo.getSelection().getEnd();
366 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
367 || _mangleTimestampsConfirmed
368 || (JOptionPane.showConfirmDialog(_frame,
369 I18nManager.getText("dialog.confirmreversetrack.text"),
370 I18nManager.getText("dialog.confirmreversetrack.title"),
371 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
373 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
374 // call track to reverse range
375 if (_track.reverseRange(selStart, selEnd))
377 _undoStack.add(undo);
379 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
385 * Complete the add time offset function with the specified offset
386 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
388 public void finishAddTimeOffset(long inTimeOffset)
390 // Construct undo information
391 int selStart = _trackInfo.getSelection().getStart();
392 int selEnd = _trackInfo.getSelection().getEnd();
393 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
394 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
396 _undoStack.add(undo);
397 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
398 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
404 * Complete the add altitude offset function with the specified offset
405 * @param inOffset altitude offset to add as String
406 * @param inFormat altitude format of offset (eg Feet, Metres)
408 public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
411 if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
414 // Construct undo information
415 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
416 int selStart = _trackInfo.getSelection().getStart();
417 int selEnd = _trackInfo.getSelection().getEnd();
418 // How many decimal places are given in the offset?
419 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
420 boolean success = false;
421 // Decimal offset given
423 double offsetd = Double.parseDouble(inOffset);
424 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
426 catch (NumberFormatException nfe) {}
429 _undoStack.add(undo);
430 _trackInfo.getSelection().markInvalid();
431 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
432 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
438 * Merge the track segments within the current selection
440 public void mergeTrackSegments()
442 if (_trackInfo.getSelection().hasRangeSelected())
444 // Maybe could check segment start flags to see if it's worth merging
445 // If first track point is already start and no other seg starts then do nothing
447 int selStart = _trackInfo.getSelection().getStart();
448 int selEnd = _trackInfo.getSelection().getEnd();
450 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
451 // Call track to merge segments
452 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
453 _undoStack.add(undo);
454 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
461 * Average the selected points
463 public void averageSelection()
465 // Find following track point
466 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
467 boolean segFlag = false;
468 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
469 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
470 // call track info object to do the averaging
471 if (_trackInfo.average())
473 _undoStack.add(undo);
479 * Create a new point at the end of the track
480 * @param inPoint point to add
482 public void createPoint(DataPoint inPoint)
484 // create undo object
485 UndoCreatePoint undo = new UndoCreatePoint();
486 _undoStack.add(undo);
487 // add point to track
488 inPoint.setSegmentStart(true);
489 _track.appendPoints(new DataPoint[] {inPoint});
490 // ensure track's field list contains point's fields
491 _track.extendFieldList(inPoint.getFieldList());
492 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
494 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
499 * Create a new point before the given position
500 * @param inPoint point to add
501 * @param inIndex index of following point
503 public void createPoint(DataPoint inPoint, int inIndex)
505 // create undo object
506 UndoInsert undo = new UndoInsert(inIndex, 1);
507 _undoStack.add(undo);
508 // add point to track
509 _track.insertPoint(inPoint, inIndex);
510 // ensure track's field list contains point's fields
511 _track.extendFieldList(inPoint.getFieldList());
512 _trackInfo.selectPoint(inIndex);
514 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
519 * Cut the current selection and move it to before the currently selected point
521 public void cutAndMoveSelection()
523 int startIndex = _trackInfo.getSelection().getStart();
524 int endIndex = _trackInfo.getSelection().getEnd();
525 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
526 // If timestamps would be mangled by cut/move, confirm
527 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
528 || _mangleTimestampsConfirmed
529 || (JOptionPane.showConfirmDialog(_frame,
530 I18nManager.getText("dialog.confirmcutandmove.text"),
531 I18nManager.getText("dialog.confirmcutandmove.title"),
532 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
534 // Find points to set segment flags
535 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
536 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
537 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
539 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
540 // Call track info to move track section
541 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
543 // Set segment start flags (first track point, next track point, move to point)
544 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
545 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
546 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
548 // Add undo object to stack, set confirm message
549 _undoStack.add(undo);
550 _trackInfo.getSelection().selectRange(-1, -1);
551 UpdateMessageBroker.informSubscribers();
552 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
560 public void selectNone()
562 // deselect point, range and photo
563 _trackInfo.getSelection().clearAll();
564 _track.clearDeletionMarkers();
568 * Receive loaded data and determine whether to filter on tracks or not
569 * @param inFieldArray array of fields
570 * @param inDataArray array of data
571 * @param inAltFormat altitude format
572 * @param inSourceInfo information about the source of the data
573 * @param inTrackNameList information about the track names
575 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
576 Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
578 // no link array given
579 informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo,
580 inTrackNameList, null);
584 * Receive loaded data and determine whether to filter on tracks or not
585 * @param inFieldArray array of fields
586 * @param inDataArray array of data
587 * @param inAltFormat altitude format
588 * @param inSourceInfo information about the source of the data
589 * @param inTrackNameList information about the track names
590 * @param inLinkInfo links to photo/audio clips
592 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
593 Altitude.Format inAltFormat, SourceInfo inSourceInfo,
594 TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
596 // Check whether loaded array can be properly parsed into a Track
597 Track loadedTrack = new Track();
598 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
599 if (loadedTrack.getNumPoints() <= 0)
601 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
602 // load next file if there's a queue
606 // Check for doubled track
607 if (Checker.isDoubledTrack(loadedTrack)) {
608 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
609 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
613 // Attach photos and/or audio clips to points
614 if (inLinkInfo != null)
616 String[] linkArray = inLinkInfo.getLinkArray();
617 if (linkArray != null) {
618 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
621 // Look at TrackNameList, decide whether to filter or not
622 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
624 // Launch a dialog to let the user choose which tracks to load, then continue
625 new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
628 // go directly to load
629 informDataLoaded(loadedTrack, inSourceInfo);
631 setCurrentMode(AppMode.NORMAL);
636 * Receive loaded data and optionally merge with current Track
637 * @param inLoadedTrack loaded track
638 * @param inSourceInfo information about the source of the data
640 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
642 // Decide whether to load or append
643 if (_track.getNumPoints() > 0)
645 // ask whether to replace or append
647 if (_autoAppendNextFile) {
648 // Automatically append the next file
649 answer = JOptionPane.YES_OPTION;
652 // Ask whether to append or not
653 answer = JOptionPane.showConfirmDialog(_frame,
654 I18nManager.getText("dialog.openappend.text"),
655 I18nManager.getText("dialog.openappend.title"),
656 JOptionPane.YES_NO_CANCEL_OPTION);
658 _autoAppendNextFile = false; // reset flag to cancel autoappend
660 if (answer == JOptionPane.YES_OPTION)
662 // append data to current Track
663 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
664 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
665 _undoStack.add(undo);
666 _track.combine(inLoadedTrack);
667 // set source information
668 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
669 _trackInfo.getFileInfo().addSource(inSourceInfo);
671 else if (answer == JOptionPane.NO_OPTION)
673 // Don't append, replace data
674 PhotoList photos = null;
675 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
676 photos = _trackInfo.getPhotoList().cloneList();
678 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
679 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
680 _undoStack.add(undo);
681 _lastSavePosition = _undoStack.size();
682 _trackInfo.getSelection().clearAll();
683 _track.load(inLoadedTrack);
684 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
685 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
686 _trackInfo.getPhotoList().removeCorrelatedPhotos();
687 _trackInfo.getAudioList().removeCorrelatedAudios();
692 // Currently no data held, so transfer received data
693 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
694 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
695 _undoStack.add(undo);
696 _lastSavePosition = _undoStack.size();
697 _trackInfo.getSelection().clearAll();
698 _track.load(inLoadedTrack);
699 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
700 _trackInfo.getFileInfo().addSource(inSourceInfo);
702 // Update config before subscribers are told
703 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
704 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
705 UpdateMessageBroker.informSubscribers();
707 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
708 + " '" + inSourceInfo.getName() + "'");
710 _menuManager.informFileLoaded();
712 _busyLoading = false;
713 // load next file if there's a queue
718 * Inform the app that NO data was loaded, eg cancel pressed
719 * Only needed if there's another file waiting in the queue
721 public void informNoDataLoaded()
723 // Load next file if there's a queue
728 * External trigger to automatically append the next loaded file
729 * instead of prompting to replace or append
731 public void autoAppendNextFile()
733 _autoAppendNextFile = true;
737 * Load the next file in the waiting list, if any
739 private void loadNextFile()
741 if (_dataFiles == null || _dataFiles.size() == 0) {
745 new Thread(new Runnable() {
747 File f = _dataFiles.get(0);
748 _dataFiles.remove(0);
749 _autoAppendNextFile = true;
750 _fileLoader.openFile(f);
758 * Accept a list of loaded photos
759 * @param inPhotoSet Set of Photo objects
761 public void informPhotosLoaded(Set<Photo> inPhotoSet)
763 if (inPhotoSet != null && !inPhotoSet.isEmpty())
765 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
766 int numPhotosAdded = numsAdded[0];
767 int numPointsAdded = numsAdded[1];
768 if (numPhotosAdded > 0)
770 // Save numbers so load can be undone
771 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
773 if (numPhotosAdded == 1) {
774 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
777 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
779 // MAYBE: Improve message when photo(s) fail to load (eg already added)
780 UpdateMessageBroker.informSubscribers();
782 if (numPointsAdded > 0) _menuManager.informFileLoaded();
788 * Save the coordinates of photos in their exif data
790 public void saveExif()
792 ExifSaver saver = new ExifSaver(_frame);
793 saver.saveExifInformation(_trackInfo.getPhotoList());
798 * Inform the app that the data has been saved
800 public void informDataSaved()
802 _lastSavePosition = _undoStack.size();
809 public void beginUndo()
811 if (_undoStack.isEmpty())
814 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
815 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
819 new UndoManager(this, _frame);
825 * Clear the undo stack (losing all undo information
827 public void clearUndo()
829 // Exit if nothing to undo
830 if (_undoStack == null || _undoStack.isEmpty())
832 // Has track got unsaved data?
833 boolean unsaved = hasDataUnsaved();
834 // Confirm operation with dialog
835 int answer = JOptionPane.showConfirmDialog(_frame,
836 I18nManager.getText("dialog.clearundo.text"),
837 I18nManager.getText("dialog.clearundo.title"),
838 JOptionPane.YES_NO_OPTION);
839 if (answer == JOptionPane.YES_OPTION)
842 _lastSavePosition = 0;
843 if (unsaved) _lastSavePosition = -1;
844 UpdateMessageBroker.informSubscribers();
850 * Undo the specified number of actions
851 * @param inNumUndos number of actions to undo
853 public void undoActions(int inNumUndos)
857 for (int i=0; i<inNumUndos; i++)
859 _undoStack.pop().performUndo(_trackInfo);
861 String message = "" + inNumUndos + " "
862 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
863 UpdateMessageBroker.informSubscribers(message);
865 catch (UndoException ue)
867 showErrorMessageNoLookup("error.undofailed.title",
868 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
871 catch (EmptyStackException empty) {}
872 UpdateMessageBroker.informSubscribers();
877 * Show a map url in an external browser
878 * @param inSourceIndex index of map source to use
880 public void showExternalMap(int inSourceIndex)
882 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
886 * Display a standard error message
887 * @param inTitleKey key to lookup for window title
888 * @param inMessageKey key to lookup for error message
890 public void showErrorMessage(String inTitleKey, String inMessageKey)
892 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
893 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
897 * Display a standard error message
898 * @param inTitleKey key to lookup for window title
899 * @param inMessage error message
901 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
903 JOptionPane.showMessageDialog(_frame, inMessage,
904 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
908 * @param inViewport viewport object
910 public void setViewport(Viewport inViewport)
912 _viewport = inViewport;
916 * @return current viewport object
918 public Viewport getViewport()
924 * Set the controller for the full screen mode
925 * @param inController controller object
927 public void setSidebarController(SidebarController inController)
929 _sidebarController = inController;
933 * Toggle sidebars on and off
935 public void toggleSidebars()
937 _sidebarController.toggle();
940 /** @return true if App is currently busy with loading data */
941 public boolean isBusyLoading() {
945 /** @return current app mode */
946 public AppMode getCurrentMode() {
950 /** @param inMode the current app mode */
951 public void setCurrentMode(AppMode inMode) {