]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/App.java
Version 14, October 2012
[GpsPrune.git] / tim / prune / App.java
1 package tim.prune;
2
3 import java.io.File;
4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
6 import java.util.Set;
7 import java.util.Stack;
8
9 import javax.swing.JFrame;
10 import javax.swing.JOptionPane;
11
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.*;
44
45
46 /**
47  * Main controller for the application
48  */
49 public class App
50 {
51         // Instance variables
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;
68
69         /** Enum for the app mode - currently only two options but may expand later */
70         public enum AppMode {NORMAL, DRAWRECT};
71
72
73         /**
74          * Constructor
75          * @param inFrame frame object for application
76          */
77         public App(JFrame inFrame)
78         {
79                 _frame = inFrame;
80                 _undoStack = new Stack<UndoOperation>();
81                 _track = new Track();
82                 _trackInfo = new TrackInfo(_track);
83                 FunctionLibrary.initialise(this);
84         }
85
86
87         /**
88          * @return the current TrackInfo
89          */
90         public TrackInfo getTrackInfo()
91         {
92                 return _trackInfo;
93         }
94
95         /**
96          * @return the dialog frame
97          */
98         public JFrame getFrame()
99         {
100                 return _frame;
101         }
102
103         /**
104          * Check if the application has unsaved data
105          * @return true if data is unsaved, false otherwise
106          */
107         public boolean hasDataUnsaved()
108         {
109                 return (_undoStack.size() > _lastSavePosition
110                         && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
111         }
112
113         /**
114          * @return the undo stack
115          */
116         public Stack<UndoOperation> getUndoStack()
117         {
118                 return _undoStack;
119         }
120
121         /**
122          * Load the specified data files one by one
123          * @param inDataFiles arraylist containing File objects to load
124          */
125         public void loadDataFiles(ArrayList<File> inDataFiles)
126         {
127                 if (inDataFiles == null || inDataFiles.size() == 0) {
128                         _dataFiles = null;
129                 }
130                 else
131                 {
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);
140                 }
141         }
142
143         /**
144          * Complete a function execution
145          * @param inUndo undo object to be added to stack
146          * @param inConfirmText confirmation text
147          */
148         public void completeFunction(UndoOperation inUndo, String inConfirmText)
149         {
150                 _undoStack.add(inUndo);
151                 UpdateMessageBroker.informSubscribers(inConfirmText);
152                 setCurrentMode(AppMode.NORMAL);
153         }
154
155         /**
156          * Set the MenuManager object to be informed about changes
157          * @param inManager MenuManager object
158          */
159         public void setMenuManager(MenuManager inManager)
160         {
161                 _menuManager = inManager;
162         }
163
164
165         /**
166          * Open a file containing track or waypoint data
167          */
168         public void openFile()
169         {
170                 if (_fileLoader == null)
171                         _fileLoader = new FileLoader(this, _frame);
172                 _fileLoader.openFile();
173         }
174
175
176         /**
177          * Add a photo or a directory of photos
178          */
179         public void addPhotos()
180         {
181                 if (_jpegLoader == null)
182                         _jpegLoader = new JpegLoader(this, _frame);
183                 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
184         }
185
186         /**
187          * Save the file in the selected format
188          */
189         public void saveFile()
190         {
191                 if (_track == null) {
192                         showErrorMessage("error.save.dialogtitle", "error.save.nodata");
193                 }
194                 else
195                 {
196                         if (_fileSaver == null) {
197                                 _fileSaver = new FileSaver(this, _frame);
198                         }
199                         char delim = ',';
200                         if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
201                         _fileSaver.showDialog(delim);
202                 }
203         }
204
205
206         /**
207          * Exit the application if confirmed
208          */
209         public void exit()
210         {
211                 // grab focus
212                 _frame.toFront();
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)
221                 {
222                         // save settings
223                         if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
224                                 new SaveConfig(this).silentSave();
225                         }
226                         System.exit(0);
227                 }
228         }
229
230
231         /**
232          * Edit the currently selected point
233          */
234         public void editCurrentPoint()
235         {
236                 if (_track != null)
237                 {
238                         DataPoint currentPoint = _trackInfo.getCurrentPoint();
239                         if (currentPoint != null)
240                         {
241                                 // Open point dialog to display details
242                                 PointEditor editor = new PointEditor(this, _frame);
243                                 editor.showDialog(_track, currentPoint);
244                         }
245                 }
246         }
247
248
249         /**
250          * Complete the point edit
251          * @param inEditList field values to edit
252          * @param inUndoList field values before edit
253          */
254         public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
255         {
256                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
257                 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
258                 {
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))
263                         {
264                                 _undoStack.push(undo);
265                                 // Confirm point edit
266                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
267                         }
268                 }
269         }
270
271
272         /**
273          * Delete the currently selected point
274          */
275         public void deleteCurrentPoint()
276         {
277                 if (_track == null) {return;}
278                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
279                 if (currentPoint != null)
280                 {
281                         // Check for photo
282                         boolean deletePhoto = false;
283                         Photo currentPhoto = currentPoint.getPhoto();
284                         if (currentPhoto != null)
285                         {
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)
292                                 {
293                                         // cancel pressed- abort delete
294                                         return;
295                                 }
296                                 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
297                         }
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())
308                         {
309                                 // Delete was successful so add undo info to stack
310                                 _undoStack.push(undo);
311                                 if (currentPhoto != null)
312                                 {
313                                         // delete photo if necessary
314                                         if (deletePhoto)
315                                         {
316                                                 _trackInfo.getPhotoList().deletePhoto(photoIndex);
317                                         }
318                                         else
319                                         {
320                                                 // decouple photo from point
321                                                 currentPhoto.setDataPoint(null);
322                                         }
323                                         UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
324                                 }
325                                 // Delete audio object (without bothering to ask)
326                                 if (audioIndex > -1) {
327                                         _trackInfo.getAudioList().deleteAudio(audioIndex);
328                                 }
329                                 // Confirm
330                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
331                                 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
332                         }
333                 }
334         }
335
336
337         /**
338          * Finish the compression by deleting the marked points
339          */
340         public void finishCompressTrack()
341         {
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)
347                 {
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")));
352                 }
353                 else {
354                         showErrorMessage("function.compress", "dialog.deletemarked.nonefound");
355                 }
356         }
357
358         /**
359          * Reverse the currently selected section of the track
360          */
361         public void reverseRange()
362         {
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)))
372                 {
373                         UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
374                         // call track to reverse range
375                         if (_track.reverseRange(selStart, selEnd))
376                         {
377                                 _undoStack.add(undo);
378                                 // Confirm
379                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
380                         }
381                 }
382         }
383
384         /**
385          * Complete the add time offset function with the specified offset
386          * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
387          */
388         public void finishAddTimeOffset(long inTimeOffset)
389         {
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))
395                 {
396                         _undoStack.add(undo);
397                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
398                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
399                 }
400         }
401
402
403         /**
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)
407          */
408         public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
409         {
410                 // Sanity check
411                 if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
412                         return;
413                 }
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
422                 try {
423                         double offsetd = Double.parseDouble(inOffset);
424                         success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
425                 }
426                 catch (NumberFormatException nfe) {}
427                 if (success)
428                 {
429                         _undoStack.add(undo);
430                         _trackInfo.getSelection().markInvalid();
431                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
432                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
433                 }
434         }
435
436
437         /**
438          * Merge the track segments within the current selection
439          */
440         public void mergeTrackSegments()
441         {
442                 if (_trackInfo.getSelection().hasRangeSelected())
443                 {
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
446
447                         int selStart = _trackInfo.getSelection().getStart();
448                         int selEnd = _trackInfo.getSelection().getEnd();
449                         // Make undo object
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"));
455                         }
456                 }
457         }
458
459
460         /**
461          * Average the selected points
462          */
463         public void averageSelection()
464         {
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())
472                 {
473                         _undoStack.add(undo);
474                 }
475         }
476
477
478         /**
479          * Create a new point at the end of the track
480          * @param inPoint point to add
481          */
482         public void createPoint(DataPoint inPoint)
483         {
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);
493                 // update listeners
494                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
495         }
496
497
498         /**
499          * Create a new point before the given position
500          * @param inPoint point to add
501          * @param inIndex index of following point
502          */
503         public void createPoint(DataPoint inPoint, int inIndex)
504         {
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);
513                 // update listeners
514                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
515         }
516
517
518         /**
519          * Cut the current selection and move it to before the currently selected point
520          */
521         public void cutAndMoveSelection()
522         {
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)))
533                 {
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);
538                         // Make undo object
539                         UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
540                         // Call track info to move track section
541                         if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
542                         {
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);}
547
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"));
553                         }
554                 }
555         }
556
557         /**
558          * Select nothing
559          */
560         public void selectNone()
561         {
562                 // deselect point, range and photo
563                 _trackInfo.getSelection().clearAll();
564                 _track.clearDeletionMarkers();
565         }
566
567         /**
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
574          */
575         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
576                 Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
577         {
578                 // no link array given
579                 informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo,
580                         inTrackNameList, null);
581         }
582
583         /**
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
591          */
592         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
593                 Altitude.Format inAltFormat, SourceInfo inSourceInfo,
594                 TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
595         {
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)
600                 {
601                         showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
602                         // load next file if there's a queue
603                         loadNextFile();
604                         return;
605                 }
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);
610                 }
611
612                 _busyLoading = true;
613                 // Attach photos and/or audio clips to points
614                 if (inLinkInfo != null)
615                 {
616                         String[] linkArray = inLinkInfo.getLinkArray();
617                         if (linkArray != null) {
618                                 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
619                         }
620                 }
621                 // Look at TrackNameList, decide whether to filter or not
622                 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
623                 {
624                         // Launch a dialog to let the user choose which tracks to load, then continue
625                         new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
626                 }
627                 else {
628                         // go directly to load
629                         informDataLoaded(loadedTrack, inSourceInfo);
630                 }
631                 setCurrentMode(AppMode.NORMAL);
632         }
633
634
635         /**
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
639          */
640         public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
641         {
642                 // Decide whether to load or append
643                 if (_track.getNumPoints() > 0)
644                 {
645                         // ask whether to replace or append
646                         int answer = 0;
647                         if (_autoAppendNextFile) {
648                                 // Automatically append the next file
649                                 answer = JOptionPane.YES_OPTION;
650                         }
651                         else {
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);
657                         }
658                         _autoAppendNextFile = false; // reset flag to cancel autoappend
659
660                         if (answer == JOptionPane.YES_OPTION)
661                         {
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);
670                         }
671                         else if (answer == JOptionPane.NO_OPTION)
672                         {
673                                 // Don't append, replace data
674                                 PhotoList photos = null;
675                                 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
676                                         photos = _trackInfo.getPhotoList().cloneList();
677                                 }
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();
688                         }
689                 }
690                 else
691                 {
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);
701                 }
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();
706                 // Update status bar
707                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
708                         + " '" + inSourceInfo.getName() + "'");
709                 // update menu
710                 _menuManager.informFileLoaded();
711                 // Remove busy lock
712                 _busyLoading = false;
713                 // load next file if there's a queue
714                 loadNextFile();
715         }
716
717         /**
718          * Inform the app that NO data was loaded, eg cancel pressed
719          * Only needed if there's another file waiting in the queue
720          */
721         public void informNoDataLoaded()
722         {
723                 // Load next file if there's a queue
724                 loadNextFile();
725         }
726
727         /**
728          * External trigger to automatically append the next loaded file
729          * instead of prompting to replace or append
730          */
731         public void autoAppendNextFile()
732         {
733                 _autoAppendNextFile = true;
734         }
735
736         /**
737          * Load the next file in the waiting list, if any
738          */
739         private void loadNextFile()
740         {
741                 if (_dataFiles == null || _dataFiles.size() == 0) {
742                         _dataFiles = null;
743                 }
744                 else {
745                         new Thread(new Runnable() {
746                                 public void run() {
747                                         File f = _dataFiles.get(0);
748                                         _dataFiles.remove(0);
749                                         _autoAppendNextFile = true;
750                                         _fileLoader.openFile(f);
751                                 }
752                         }).start();
753                 }
754         }
755
756
757         /**
758          * Accept a list of loaded photos
759          * @param inPhotoSet Set of Photo objects
760          */
761         public void informPhotosLoaded(Set<Photo> inPhotoSet)
762         {
763                 if (inPhotoSet != null && !inPhotoSet.isEmpty())
764                 {
765                         int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
766                         int numPhotosAdded = numsAdded[0];
767                         int numPointsAdded = numsAdded[1];
768                         if (numPhotosAdded > 0)
769                         {
770                                 // Save numbers so load can be undone
771                                 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
772                         }
773                         if (numPhotosAdded == 1) {
774                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
775                         }
776                         else {
777                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
778                         }
779                         // MAYBE: Improve message when photo(s) fail to load (eg already added)
780                         UpdateMessageBroker.informSubscribers();
781                         // update menu
782                         if (numPointsAdded > 0) _menuManager.informFileLoaded();
783                 }
784         }
785
786
787         /**
788          * Save the coordinates of photos in their exif data
789          */
790         public void saveExif()
791         {
792                 ExifSaver saver = new ExifSaver(_frame);
793                 saver.saveExifInformation(_trackInfo.getPhotoList());
794         }
795
796
797         /**
798          * Inform the app that the data has been saved
799          */
800         public void informDataSaved()
801         {
802                 _lastSavePosition = _undoStack.size();
803         }
804
805
806         /**
807          * Begin undo process
808          */
809         public void beginUndo()
810         {
811                 if (_undoStack.isEmpty())
812                 {
813                         // Nothing to undo
814                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
815                                 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
816                 }
817                 else
818                 {
819                         new UndoManager(this, _frame);
820                 }
821         }
822
823
824         /**
825          * Clear the undo stack (losing all undo information
826          */
827         public void clearUndo()
828         {
829                 // Exit if nothing to undo
830                 if (_undoStack == null || _undoStack.isEmpty())
831                         return;
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)
840                 {
841                         _undoStack.clear();
842                         _lastSavePosition = 0;
843                         if (unsaved) _lastSavePosition = -1;
844                         UpdateMessageBroker.informSubscribers();
845                 }
846         }
847
848
849         /**
850          * Undo the specified number of actions
851          * @param inNumUndos number of actions to undo
852          */
853         public void undoActions(int inNumUndos)
854         {
855                 try
856                 {
857                         for (int i=0; i<inNumUndos; i++)
858                         {
859                                 _undoStack.pop().performUndo(_trackInfo);
860                         }
861                         String message = "" + inNumUndos + " "
862                                  + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
863                         UpdateMessageBroker.informSubscribers(message);
864                 }
865                 catch (UndoException ue)
866                 {
867                         showErrorMessageNoLookup("error.undofailed.title",
868                                 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
869                         _undoStack.clear();
870                 }
871                 catch (EmptyStackException empty) {}
872                 UpdateMessageBroker.informSubscribers();
873         }
874
875
876         /**
877          * Show a map url in an external browser
878          * @param inSourceIndex index of map source to use
879          */
880         public void showExternalMap(int inSourceIndex)
881         {
882                 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
883         }
884
885         /**
886          * Display a standard error message
887          * @param inTitleKey key to lookup for window title
888          * @param inMessageKey key to lookup for error message
889          */
890         public void showErrorMessage(String inTitleKey, String inMessageKey)
891         {
892                 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
893                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
894         }
895
896         /**
897          * Display a standard error message
898          * @param inTitleKey key to lookup for window title
899          * @param inMessage error message
900          */
901         public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
902         {
903                 JOptionPane.showMessageDialog(_frame, inMessage,
904                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
905         }
906
907         /**
908          * @param inViewport viewport object
909          */
910         public void setViewport(Viewport inViewport)
911         {
912                 _viewport = inViewport;
913         }
914
915         /**
916          * @return current viewport object
917          */
918         public Viewport getViewport()
919         {
920                 return _viewport;
921         }
922
923         /**
924          * Set the controller for the full screen mode
925          * @param inController controller object
926          */
927         public void setSidebarController(SidebarController inController)
928         {
929                 _sidebarController = inController;
930         }
931
932         /**
933          * Toggle sidebars on and off
934          */
935         public void toggleSidebars()
936         {
937                 _sidebarController.toggle();
938         }
939
940         /** @return true if App is currently busy with loading data */
941         public boolean isBusyLoading() {
942                 return _busyLoading;
943         }
944
945         /** @return current app mode */
946         public AppMode getCurrentMode() {
947                 return _appMode;
948         }
949
950         /** @param inMode the current app mode */
951         public void setCurrentMode(AppMode inMode) {
952                 _appMode = inMode;
953         }
954 }