]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/App.java
Version 16, February 2014
[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.tips.TipManager;
45 import tim.prune.undo.*;
46
47
48 /**
49  * Main controller for the application
50  */
51 public class App
52 {
53         // Instance variables
54         private JFrame _frame = null;
55         private Track _track = null;
56         private TrackInfo _trackInfo = null;
57         private int _lastSavePosition = 0;
58         private MenuManager _menuManager = null;
59         private SidebarController _sidebarController = null;
60         private FileLoader _fileLoader = null;
61         private JpegLoader _jpegLoader = null;
62         private FileSaver _fileSaver = null;
63         private Stack<UndoOperation> _undoStack = null;
64         private boolean _mangleTimestampsConfirmed = false;
65         private Viewport _viewport = null;
66         private ArrayList<File> _dataFiles = null;
67         private boolean _autoAppendNextFile = false;
68         private boolean _busyLoading = false;
69         private AppMode _appMode = AppMode.NORMAL;
70
71         /** Enum for the app mode - currently only two options but may expand later */
72         public enum AppMode {NORMAL, DRAWRECT};
73
74
75         /**
76          * Constructor
77          * @param inFrame frame object for application
78          */
79         public App(JFrame inFrame)
80         {
81                 _frame = inFrame;
82                 _undoStack = new Stack<UndoOperation>();
83                 _track = new Track();
84                 _trackInfo = new TrackInfo(_track);
85                 FunctionLibrary.initialise(this);
86         }
87
88
89         /**
90          * @return the current TrackInfo
91          */
92         public TrackInfo getTrackInfo()
93         {
94                 return _trackInfo;
95         }
96
97         /**
98          * @return the dialog frame
99          */
100         public JFrame getFrame()
101         {
102                 return _frame;
103         }
104
105         /**
106          * Check if the application has unsaved data
107          * @return true if data is unsaved, false otherwise
108          */
109         public boolean hasDataUnsaved()
110         {
111                 return (_undoStack.size() > _lastSavePosition
112                         && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
113         }
114
115         /**
116          * @return the undo stack
117          */
118         public Stack<UndoOperation> getUndoStack()
119         {
120                 return _undoStack;
121         }
122
123
124         /**
125          * Show the specified tip if appropriate
126          * @param inTipNumber tip number from TipManager
127          */
128         public void showTip(int inTipNumber)
129         {
130                 String key = TipManager.fireTipTrigger(inTipNumber);
131                 if (key != null && !key.equals(""))
132                 {
133                         JOptionPane.showMessageDialog(_frame, I18nManager.getText(key),
134                                 I18nManager.getText("tip.title"), JOptionPane.INFORMATION_MESSAGE);
135                 }
136         }
137
138
139         /**
140          * Load the specified data files one by one
141          * @param inDataFiles arraylist containing File objects to load
142          */
143         public void loadDataFiles(ArrayList<File> inDataFiles)
144         {
145                 if (inDataFiles == null || inDataFiles.size() == 0) {
146                         _dataFiles = null;
147                 }
148                 else
149                 {
150                         _dataFiles = inDataFiles;
151                         File f = _dataFiles.get(0);
152                         _dataFiles.remove(0);
153                         // Start load of specified file
154                         if (_fileLoader == null)
155                                 _fileLoader = new FileLoader(this, _frame);
156                         _autoAppendNextFile = false; // prompt for append
157                         _fileLoader.openFile(f);
158                 }
159         }
160
161         /**
162          * Complete a function execution
163          * @param inUndo undo object to be added to stack
164          * @param inConfirmText confirmation text
165          */
166         public void completeFunction(UndoOperation inUndo, String inConfirmText)
167         {
168                 _undoStack.add(inUndo);
169                 UpdateMessageBroker.informSubscribers(inConfirmText);
170                 setCurrentMode(AppMode.NORMAL);
171         }
172
173         /**
174          * Set the MenuManager object to be informed about changes
175          * @param inManager MenuManager object
176          */
177         public void setMenuManager(MenuManager inManager)
178         {
179                 _menuManager = inManager;
180         }
181
182
183         /**
184          * Open a file containing track or waypoint data
185          */
186         public void openFile()
187         {
188                 if (_fileLoader == null)
189                         _fileLoader = new FileLoader(this, _frame);
190                 _fileLoader.openFile();
191         }
192
193
194         /**
195          * Add a photo or a directory of photos
196          */
197         public void addPhotos()
198         {
199                 if (_jpegLoader == null)
200                         _jpegLoader = new JpegLoader(this, _frame);
201                 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
202         }
203
204         /**
205          * Save the file in the selected format
206          */
207         public void saveFile()
208         {
209                 if (_track == null) {
210                         showErrorMessage("error.save.dialogtitle", "error.save.nodata");
211                 }
212                 else
213                 {
214                         if (_fileSaver == null) {
215                                 _fileSaver = new FileSaver(this, _frame);
216                         }
217                         char delim = ',';
218                         if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
219                         _fileSaver.showDialog(delim);
220                 }
221         }
222
223
224         /**
225          * Exit the application if confirmed
226          */
227         public void exit()
228         {
229                 // grab focus
230                 _frame.toFront();
231                 _frame.requestFocus();
232                 // check if ok to exit
233                 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
234                 if (!hasDataUnsaved()
235                         || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
236                                 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
237                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
238                         == JOptionPane.YES_OPTION)
239                 {
240                         // save settings
241                         if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
242                                 new SaveConfig(this).silentSave();
243                         }
244                         System.exit(0);
245                 }
246         }
247
248
249         /**
250          * Edit the currently selected point
251          */
252         public void editCurrentPoint()
253         {
254                 if (_track != null)
255                 {
256                         DataPoint currentPoint = _trackInfo.getCurrentPoint();
257                         if (currentPoint != null)
258                         {
259                                 // Open point dialog to display details
260                                 PointEditor editor = new PointEditor(this, _frame);
261                                 editor.showDialog(_track, currentPoint);
262                         }
263                 }
264         }
265
266
267         /**
268          * Complete the point edit
269          * @param inEditList field values to edit
270          * @param inUndoList field values before edit
271          */
272         public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
273         {
274                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
275                 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
276                 {
277                         // add information to undo stack
278                         UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
279                         // pass to track for completion
280                         if (_track.editPoint(currentPoint, inEditList, false))
281                         {
282                                 _undoStack.push(undo);
283                                 // Confirm point edit
284                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
285                         }
286                 }
287         }
288
289
290         /**
291          * Delete the currently selected point
292          */
293         public void deleteCurrentPoint()
294         {
295                 if (_track == null) {return;}
296                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
297                 if (currentPoint != null)
298                 {
299                         // Check for photo
300                         boolean deletePhoto = false;
301                         Photo currentPhoto = currentPoint.getPhoto();
302                         if (currentPhoto != null)
303                         {
304                                 // Confirm deletion of photo or decoupling
305                                 int response = JOptionPane.showConfirmDialog(_frame,
306                                         I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
307                                         I18nManager.getText("dialog.deletepoint.title"),
308                                         JOptionPane.YES_NO_CANCEL_OPTION);
309                                 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
310                                 {
311                                         // cancel pressed- abort delete
312                                         return;
313                                 }
314                                 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
315                         }
316                         // store necessary information to undo it later
317                         int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
318                         int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
319                         int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
320                         DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
321                         // Construct Undo object
322                         UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
323                                 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
324                         // call track to delete point
325                         if (_trackInfo.deletePoint())
326                         {
327                                 // Delete was successful so add undo info to stack
328                                 _undoStack.push(undo);
329                                 if (currentPhoto != null)
330                                 {
331                                         // delete photo if necessary
332                                         if (deletePhoto)
333                                         {
334                                                 _trackInfo.getPhotoList().deletePhoto(photoIndex);
335                                         }
336                                         else
337                                         {
338                                                 // decouple photo from point
339                                                 currentPhoto.setDataPoint(null);
340                                         }
341                                         UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
342                                 }
343                                 // Delete audio object (without bothering to ask)
344                                 if (audioIndex > -1) {
345                                         _trackInfo.getAudioList().deleteAudio(audioIndex);
346                                 }
347                                 // Confirm
348                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
349                                 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
350                         }
351                 }
352         }
353
354
355         /**
356          * Finish the compression by deleting the marked points
357          */
358         public void finishCompressTrack()
359         {
360                 UndoDeleteMarked undo = new UndoDeleteMarked(_track);
361                 // call track to do compress
362                 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
363                 // add to undo stack if successful
364                 if (numPointsDeleted > 0)
365                 {
366                         undo.setNumPointsDeleted(numPointsDeleted);
367                         _undoStack.add(undo);
368                         UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
369                                  + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
370                 }
371                 else {
372                         showErrorMessage("function.compress", "dialog.deletemarked.nonefound");
373                 }
374         }
375
376         /**
377          * Reverse the currently selected section of the track
378          */
379         public void reverseRange()
380         {
381                 // check whether Timestamp field exists, and if so confirm reversal
382                 int selStart = _trackInfo.getSelection().getStart();
383                 int selEnd = _trackInfo.getSelection().getEnd();
384                 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
385                         || _mangleTimestampsConfirmed
386                         || (JOptionPane.showConfirmDialog(_frame,
387                                  I18nManager.getText("dialog.confirmreversetrack.text"),
388                                  I18nManager.getText("dialog.confirmreversetrack.title"),
389                                  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
390                 {
391                         UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
392                         // call track to reverse range
393                         if (_track.reverseRange(selStart, selEnd))
394                         {
395                                 _undoStack.add(undo);
396                                 // Confirm
397                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
398                         }
399                 }
400         }
401
402         /**
403          * Complete the add time offset function with the specified offset
404          * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
405          */
406         public void finishAddTimeOffset(long inTimeOffset)
407         {
408                 // Construct undo information
409                 int selStart = _trackInfo.getSelection().getStart();
410                 int selEnd = _trackInfo.getSelection().getEnd();
411                 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
412                 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
413                 {
414                         _undoStack.add(undo);
415                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
416                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
417                 }
418         }
419
420
421         /**
422          * Complete the add altitude offset function with the specified offset
423          * @param inOffset altitude offset to add as String
424          * @param inUnit altitude units of offset (eg Feet, Metres)
425          */
426         public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
427         {
428                 // Sanity check
429                 if (inOffset == null || inOffset.equals("") || inUnit == null) {
430                         return;
431                 }
432                 // Construct undo information
433                 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
434                 int selStart = _trackInfo.getSelection().getStart();
435                 int selEnd = _trackInfo.getSelection().getEnd();
436                 // How many decimal places are given in the offset?
437                 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
438                 boolean success = false;
439                 // Decimal offset given
440                 try {
441                         double offsetd = Double.parseDouble(inOffset);
442                         success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
443                 }
444                 catch (NumberFormatException nfe) {}
445                 if (success)
446                 {
447                         _undoStack.add(undo);
448                         _trackInfo.getSelection().markInvalid();
449                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
450                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
451                 }
452         }
453
454
455         /**
456          * Merge the track segments within the current selection
457          */
458         public void mergeTrackSegments()
459         {
460                 if (_trackInfo.getSelection().hasRangeSelected())
461                 {
462                         // Maybe could check segment start flags to see if it's worth merging
463                         // If first track point is already start and no other seg starts then do nothing
464
465                         int selStart = _trackInfo.getSelection().getStart();
466                         int selEnd = _trackInfo.getSelection().getEnd();
467                         // Make undo object
468                         UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
469                         // Call track to merge segments
470                         if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
471                                 _undoStack.add(undo);
472                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
473                         }
474                 }
475         }
476
477
478         /**
479          * Average the selected points
480          */
481         public void averageSelection()
482         {
483                 // Find following track point
484                 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
485                 boolean segFlag = false;
486                 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
487                 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
488                 // call track info object to do the averaging
489                 if (_trackInfo.average())
490                 {
491                         _undoStack.add(undo);
492                 }
493         }
494
495
496         /**
497          * Create a new point at the end of the track
498          * @param inPoint point to add
499          */
500         public void createPoint(DataPoint inPoint)
501         {
502                 // create undo object
503                 UndoCreatePoint undo = new UndoCreatePoint();
504                 _undoStack.add(undo);
505                 // add point to track
506                 inPoint.setSegmentStart(true);
507                 _track.appendPoints(new DataPoint[] {inPoint});
508                 // ensure track's field list contains point's fields
509                 _track.extendFieldList(inPoint.getFieldList());
510                 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
511                 // update listeners
512                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
513         }
514
515
516         /**
517          * Create a new point before the given position
518          * @param inPoint point to add
519          * @param inIndex index of following point
520          */
521         public void createPoint(DataPoint inPoint, int inIndex)
522         {
523                 // create undo object
524                 UndoInsert undo = new UndoInsert(inIndex, 1);
525                 _undoStack.add(undo);
526                 // add point to track
527                 _track.insertPoint(inPoint, inIndex);
528                 // ensure track's field list contains point's fields
529                 _track.extendFieldList(inPoint.getFieldList());
530                 _trackInfo.selectPoint(inIndex);
531                 final int selStart = _trackInfo.getSelection().getStart();
532                 final int selEnd   = _trackInfo.getSelection().getEnd();
533                 if (selStart < inIndex && selEnd > inIndex)
534                 {
535                         // Extend end of selection by 1
536                         _trackInfo.getSelection().selectRange(selStart, selEnd+1);
537                 }
538                 // update listeners
539                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
540         }
541
542
543         /**
544          * Cut the current selection and move it to before the currently selected point
545          */
546         public void cutAndMoveSelection()
547         {
548                 int startIndex = _trackInfo.getSelection().getStart();
549                 int endIndex = _trackInfo.getSelection().getEnd();
550                 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
551                 // If timestamps would be mangled by cut/move, confirm
552                 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
553                         || _mangleTimestampsConfirmed
554                         || (JOptionPane.showConfirmDialog(_frame,
555                                  I18nManager.getText("dialog.confirmcutandmove.text"),
556                                  I18nManager.getText("dialog.confirmcutandmove.title"),
557                                  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
558                 {
559                         // Find points to set segment flags
560                         DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
561                         DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
562                         DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
563                         // Make undo object
564                         UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
565                         // Call track info to move track section
566                         if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
567                         {
568                                 // Set segment start flags (first track point, next track point, move to point)
569                                 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
570                                 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
571                                 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
572
573                                 // Add undo object to stack, set confirm message
574                                 _undoStack.add(undo);
575                                 _trackInfo.getSelection().selectRange(-1, -1);
576                                 UpdateMessageBroker.informSubscribers();
577                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
578                         }
579                 }
580         }
581
582         /**
583          * Select nothing
584          */
585         public void selectNone()
586         {
587                 // deselect point, range and photo
588                 _trackInfo.getSelection().clearAll();
589                 _track.clearDeletionMarkers();
590         }
591
592         /**
593          * Receive loaded data and determine whether to filter on tracks or not
594          * @param inFieldArray array of fields
595          * @param inDataArray array of data
596          * @param inSourceInfo information about the source of the data
597          * @param inTrackNameList information about the track names
598          */
599         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
600                 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
601         {
602                 // no link array given
603                 informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
604                         inTrackNameList, null);
605         }
606
607         /**
608          * Receive loaded data and determine whether to filter on tracks or not
609          * @param inFieldArray array of fields
610          * @param inDataArray array of data
611          * @param inOptions creation options such as units
612          * @param inSourceInfo information about the source of the data
613          * @param inTrackNameList information about the track names
614          */
615         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
616                 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
617         {
618                 // no link array given
619                 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
620                         inTrackNameList, null);
621         }
622
623         /**
624          * Receive loaded data and determine whether to filter on tracks or not
625          * @param inFieldArray array of fields
626          * @param inDataArray array of data
627          * @param inOptions creation options such as units
628          * @param inSourceInfo information about the source of the data
629          * @param inTrackNameList information about the track names
630          * @param inLinkInfo links to photo/audio clips
631          */
632         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
633                 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
634         {
635                 // Check whether loaded array can be properly parsed into a Track
636                 Track loadedTrack = new Track();
637                 loadedTrack.load(inFieldArray, inDataArray, inOptions);
638                 if (loadedTrack.getNumPoints() <= 0)
639                 {
640                         showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
641                         // load next file if there's a queue
642                         loadNextFile();
643                         return;
644                 }
645                 // Check for doubled track
646                 if (Checker.isDoubledTrack(loadedTrack)) {
647                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
648                                 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
649                 }
650
651                 _busyLoading = true;
652                 // Attach photos and/or audio clips to points
653                 if (inLinkInfo != null)
654                 {
655                         String[] linkArray = inLinkInfo.getLinkArray();
656                         if (linkArray != null) {
657                                 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
658                         }
659                 }
660                 // Look at TrackNameList, decide whether to filter or not
661                 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
662                 {
663                         // Launch a dialog to let the user choose which tracks to load, then continue
664                         new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
665                 }
666                 else {
667                         // go directly to load
668                         informDataLoaded(loadedTrack, inSourceInfo);
669                 }
670                 setCurrentMode(AppMode.NORMAL);
671         }
672
673
674         /**
675          * Receive loaded data and optionally merge with current Track
676          * @param inLoadedTrack loaded track
677          * @param inSourceInfo information about the source of the data
678          */
679         public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
680         {
681                 // Decide whether to load or append
682                 if (_track.getNumPoints() > 0)
683                 {
684                         // ask whether to replace or append
685                         int answer = 0;
686                         if (_autoAppendNextFile) {
687                                 // Automatically append the next file
688                                 answer = JOptionPane.YES_OPTION;
689                         }
690                         else {
691                                 // Ask whether to append or not
692                                 answer = JOptionPane.showConfirmDialog(_frame,
693                                         I18nManager.getText("dialog.openappend.text"),
694                                         I18nManager.getText("dialog.openappend.title"),
695                                         JOptionPane.YES_NO_CANCEL_OPTION);
696                         }
697                         _autoAppendNextFile = false; // reset flag to cancel autoappend
698
699                         if (answer == JOptionPane.YES_OPTION)
700                         {
701                                 // append data to current Track
702                                 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
703                                 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
704                                 _undoStack.add(undo);
705                                 _track.combine(inLoadedTrack);
706                                 // set source information
707                                 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
708                                 _trackInfo.getFileInfo().addSource(inSourceInfo);
709                         }
710                         else if (answer == JOptionPane.NO_OPTION)
711                         {
712                                 // Don't append, replace data
713                                 PhotoList photos = null;
714                                 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
715                                         photos = _trackInfo.getPhotoList().cloneList();
716                                 }
717                                 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
718                                 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
719                                 _undoStack.add(undo);
720                                 _lastSavePosition = _undoStack.size();
721                                 _trackInfo.getSelection().clearAll();
722                                 _track.load(inLoadedTrack);
723                                 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
724                                 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
725                                 _trackInfo.getPhotoList().removeCorrelatedPhotos();
726                                 _trackInfo.getAudioList().removeCorrelatedAudios();
727                         }
728                 }
729                 else
730                 {
731                         // Currently no data held, so transfer received data
732                         UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
733                         undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
734                         _undoStack.add(undo);
735                         _lastSavePosition = _undoStack.size();
736                         _trackInfo.getSelection().clearAll();
737                         _track.load(inLoadedTrack);
738                         inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
739                         _trackInfo.getFileInfo().addSource(inSourceInfo);
740                 }
741                 // Update config before subscribers are told
742                 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
743                 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
744                 UpdateMessageBroker.informSubscribers();
745                 // Update status bar
746                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
747                         + " '" + inSourceInfo.getName() + "'");
748                 // update menu
749                 _menuManager.informFileLoaded();
750                 // Remove busy lock
751                 _busyLoading = false;
752                 // load next file if there's a queue
753                 loadNextFile();
754         }
755
756         /**
757          * Inform the app that NO data was loaded, eg cancel pressed
758          * Only needed if there's another file waiting in the queue
759          */
760         public void informNoDataLoaded()
761         {
762                 // Load next file if there's a queue
763                 loadNextFile();
764         }
765
766         /**
767          * External trigger to automatically append the next loaded file
768          * instead of prompting to replace or append
769          */
770         public void autoAppendNextFile()
771         {
772                 _autoAppendNextFile = true;
773         }
774
775         /**
776          * Load the next file in the waiting list, if any
777          */
778         private void loadNextFile()
779         {
780                 if (_dataFiles == null || _dataFiles.size() == 0) {
781                         _dataFiles = null;
782                 }
783                 else {
784                         new Thread(new Runnable() {
785                                 public void run() {
786                                         File f = _dataFiles.get(0);
787                                         _dataFiles.remove(0);
788                                         _autoAppendNextFile = true;
789                                         _fileLoader.openFile(f);
790                                 }
791                         }).start();
792                 }
793         }
794
795
796         /**
797          * Accept a list of loaded photos
798          * @param inPhotoSet Set of Photo objects
799          */
800         public void informPhotosLoaded(Set<Photo> inPhotoSet)
801         {
802                 if (inPhotoSet != null && !inPhotoSet.isEmpty())
803                 {
804                         int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
805                         int numPhotosAdded = numsAdded[0];
806                         int numPointsAdded = numsAdded[1];
807                         if (numPhotosAdded > 0)
808                         {
809                                 // Save numbers so load can be undone
810                                 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
811                         }
812                         if (numPhotosAdded == 1) {
813                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
814                         }
815                         else {
816                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
817                         }
818                         // MAYBE: Improve message when photo(s) fail to load (eg already added)
819                         UpdateMessageBroker.informSubscribers();
820                         // update menu
821                         if (numPointsAdded > 0) _menuManager.informFileLoaded();
822                 }
823         }
824
825
826         /**
827          * Save the coordinates of photos in their exif data
828          */
829         public void saveExif()
830         {
831                 ExifSaver saver = new ExifSaver(_frame);
832                 saver.saveExifInformation(_trackInfo.getPhotoList());
833         }
834
835
836         /**
837          * Inform the app that the data has been saved
838          */
839         public void informDataSaved()
840         {
841                 _lastSavePosition = _undoStack.size();
842         }
843
844
845         /**
846          * Begin undo process
847          */
848         public void beginUndo()
849         {
850                 if (_undoStack.isEmpty())
851                 {
852                         // Nothing to undo
853                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
854                                 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
855                 }
856                 else
857                 {
858                         new UndoManager(this, _frame);
859                 }
860         }
861
862
863         /**
864          * Clear the undo stack (losing all undo information
865          */
866         public void clearUndo()
867         {
868                 // Exit if nothing to undo
869                 if (_undoStack == null || _undoStack.isEmpty())
870                         return;
871                 // Has track got unsaved data?
872                 boolean unsaved = hasDataUnsaved();
873                 // Confirm operation with dialog
874                 int answer = JOptionPane.showConfirmDialog(_frame,
875                         I18nManager.getText("dialog.clearundo.text"),
876                         I18nManager.getText("dialog.clearundo.title"),
877                         JOptionPane.YES_NO_OPTION);
878                 if (answer == JOptionPane.YES_OPTION)
879                 {
880                         _undoStack.clear();
881                         _lastSavePosition = 0;
882                         if (unsaved) _lastSavePosition = -1;
883                         UpdateMessageBroker.informSubscribers();
884                 }
885         }
886
887
888         /**
889          * Undo the specified number of actions
890          * @param inNumUndos number of actions to undo
891          */
892         public void undoActions(int inNumUndos)
893         {
894                 try
895                 {
896                         for (int i=0; i<inNumUndos; i++)
897                         {
898                                 _undoStack.pop().performUndo(_trackInfo);
899                         }
900                         String message = "" + inNumUndos + " "
901                                  + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
902                         UpdateMessageBroker.informSubscribers(message);
903                 }
904                 catch (UndoException ue)
905                 {
906                         showErrorMessageNoLookup("error.undofailed.title",
907                                 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
908                         _undoStack.clear();
909                 }
910                 catch (EmptyStackException empty) {}
911                 UpdateMessageBroker.informSubscribers();
912         }
913
914
915         /**
916          * Show a map url in an external browser
917          * @param inSourceIndex index of map source to use
918          */
919         public void showExternalMap(int inSourceIndex)
920         {
921                 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
922         }
923
924         /**
925          * Display a standard error message
926          * @param inTitleKey key to lookup for window title
927          * @param inMessageKey key to lookup for error message
928          */
929         public void showErrorMessage(String inTitleKey, String inMessageKey)
930         {
931                 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
932                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
933         }
934
935         /**
936          * Display a standard error message
937          * @param inTitleKey key to lookup for window title
938          * @param inMessage error message
939          */
940         public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
941         {
942                 JOptionPane.showMessageDialog(_frame, inMessage,
943                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
944         }
945
946         /**
947          * @param inViewport viewport object
948          */
949         public void setViewport(Viewport inViewport)
950         {
951                 _viewport = inViewport;
952         }
953
954         /**
955          * @return current viewport object
956          */
957         public Viewport getViewport()
958         {
959                 return _viewport;
960         }
961
962         /**
963          * Set the controller for the full screen mode
964          * @param inController controller object
965          */
966         public void setSidebarController(SidebarController inController)
967         {
968                 _sidebarController = inController;
969         }
970
971         /**
972          * Toggle sidebars on and off
973          */
974         public void toggleSidebars()
975         {
976                 _sidebarController.toggle();
977         }
978
979         /** @return true if App is currently busy with loading data */
980         public boolean isBusyLoading() {
981                 return _busyLoading;
982         }
983
984         /** @return current app mode */
985         public AppMode getCurrentMode() {
986                 return _appMode;
987         }
988
989         /** @param inMode the current app mode */
990         public void setCurrentMode(AppMode inMode) {
991                 _appMode = inMode;
992         }
993 }