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