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