]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/App.java
Merge branch 'not-ready-for-upstreaming' into fp-integration
[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          * Remove altitudes from selected points
461          */
462         public void removeAltitudes(int selStart, int selEnd)
463         {
464                 UndoRemoveAltitudes undo = new UndoRemoveAltitudes(_trackInfo, selStart, selEnd);
465                 if (_trackInfo.getTrack().removeAltitudes(selStart, selEnd))
466                 {
467                         _undoStack.add(undo);
468                         _trackInfo.getSelection().markInvalid();
469                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
470                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.removealtitudes"));
471                 }
472         }
473
474
475         /**
476          * Merge the track segments within the current selection
477          */
478         public void mergeTrackSegments()
479         {
480                 if (_trackInfo.getSelection().hasRangeSelected())
481                 {
482                         // Maybe could check segment start flags to see if it's worth merging
483                         // If first track point is already start and no other seg starts then do nothing
484
485                         int selStart = _trackInfo.getSelection().getStart();
486                         int selEnd = _trackInfo.getSelection().getEnd();
487                         // Make undo object
488                         UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
489                         // Call track to merge segments
490                         if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
491                                 _undoStack.add(undo);
492                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
493                         }
494                 }
495         }
496
497
498         /**
499          * Average the selected points
500          */
501         public void averageSelection()
502         {
503                 // Find following track point
504                 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
505                 boolean segFlag = false;
506                 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
507                 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
508                 // call track info object to do the averaging
509                 if (_trackInfo.average())
510                 {
511                         _undoStack.add(undo);
512                 }
513         }
514
515
516         /**
517          * Create a new point at the end of the track
518          * @param inPoint point to add
519          */
520         public void createPoint(DataPoint inPoint)
521         {
522                 createPoint(inPoint, true);
523         }
524
525         /**
526          * Create a new point at the end of the track
527          * @param inPoint point to add
528          * @param inNewSegment true for a single point, false for a continuation
529          */
530         public void createPoint(DataPoint inPoint, boolean inNewSegment)
531         {
532                 // create undo object
533                 UndoCreatePoint undo = new UndoCreatePoint();
534                 _undoStack.add(undo);
535                 // add point to track
536                 inPoint.setSegmentStart(inNewSegment);
537                 _track.appendPoints(new DataPoint[] {inPoint});
538                 // ensure track's field list contains point's fields
539                 _track.extendFieldList(inPoint.getFieldList());
540                 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
541                 // update listeners
542                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
543         }
544
545
546         /**
547          * Create a new point before the given position
548          * @param inPoint point to add
549          * @param inIndex index of following point
550          */
551         public void createPoint(DataPoint inPoint, int inIndex)
552         {
553                 // create undo object
554                 UndoInsert undo = new UndoInsert(inIndex, 1);
555                 _undoStack.add(undo);
556                 // add point to track
557                 _track.insertPoint(inPoint, inIndex);
558                 // ensure track's field list contains point's fields
559                 _track.extendFieldList(inPoint.getFieldList());
560                 _trackInfo.selectPoint(inIndex);
561                 final int selStart = _trackInfo.getSelection().getStart();
562                 final int selEnd   = _trackInfo.getSelection().getEnd();
563                 if (selStart < inIndex && selEnd >= inIndex)
564                 {
565                         // Extend end of selection by 1
566                         _trackInfo.getSelection().selectRange(selStart, selEnd+1);
567                 }
568                 // update listeners
569                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
570         }
571
572
573         /**
574          * Cut the current selection and move it to before the currently selected point
575          */
576         public void cutAndMoveSelection()
577         {
578                 int startIndex = _trackInfo.getSelection().getStart();
579                 int endIndex = _trackInfo.getSelection().getEnd();
580                 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
581                 // If timestamps would be mangled by cut/move, confirm
582                 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
583                         || _mangleTimestampsConfirmed
584                         || (JOptionPane.showConfirmDialog(_frame,
585                                  I18nManager.getText("dialog.confirmcutandmove.text"),
586                                  I18nManager.getText("dialog.confirmcutandmove.title"),
587                                  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
588                 {
589                         // Find points to set segment flags
590                         DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
591                         DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
592                         DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
593                         // Make undo object
594                         UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
595                         // Call track info to move track section
596                         if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
597                         {
598                                 // Set segment start flags (first track point, next track point, move to point)
599                                 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
600                                 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
601                                 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
602
603                                 // Add undo object to stack, set confirm message
604                                 _undoStack.add(undo);
605                                 _trackInfo.getSelection().selectRange(-1, -1);
606                                 UpdateMessageBroker.informSubscribers();
607                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
608                         }
609                 }
610         }
611
612         /**
613          * Select nothing
614          */
615         public void selectNone()
616         {
617                 // deselect point, range and photo
618                 _trackInfo.getSelection().clearAll();
619                 _track.clearDeletionMarkers();
620         }
621
622         /**
623          * Receive loaded data and determine whether to filter on tracks or not
624          * @param inFieldArray array of fields
625          * @param inDataArray array of data
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                 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
631         {
632                 // no link array given
633                 informDataLoaded(inFieldArray, inDataArray, null, 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          */
645         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
646                 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
647         {
648                 // no link array given
649                 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
650                         inTrackNameList, null);
651         }
652
653         /**
654          * Receive loaded data and determine whether to filter on tracks or not
655          * @param inFieldArray array of fields
656          * @param inDataArray array of data
657          * @param inOptions creation options such as units
658          * @param inSourceInfo information about the source of the data
659          * @param inTrackNameList information about the track names
660          * @param inLinkInfo links to photo/audio clips
661          */
662         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
663                 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
664         {
665                 // Check whether loaded array can be properly parsed into a Track
666                 Track loadedTrack = new Track();
667                 loadedTrack.load(inFieldArray, inDataArray, inOptions);
668                 if (loadedTrack.getNumPoints() <= 0)
669                 {
670                         showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
671                         // load next file if there's a queue
672                         loadNextFile();
673                         return;
674                 }
675                 // Check for doubled track
676                 if (Checker.isDoubledTrack(loadedTrack)) {
677                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
678                                 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
679                 }
680
681                 _busyLoading = true;
682                 // Attach photos and/or audio clips to points
683                 if (inLinkInfo != null)
684                 {
685                         String[] linkArray = inLinkInfo.getLinkArray();
686                         if (linkArray != null) {
687                                 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
688                         }
689                 }
690                 // Look at TrackNameList, decide whether to filter or not
691                 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
692                 {
693                         // Launch a dialog to let the user choose which tracks to load, then continue
694                         new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
695                 }
696                 else {
697                         // go directly to load
698                         informDataLoaded(loadedTrack, inSourceInfo);
699                 }
700                 setCurrentMode(AppMode.NORMAL);
701         }
702
703
704         /**
705          * Receive loaded data and optionally merge with current Track
706          * @param inLoadedTrack loaded track
707          * @param inSourceInfo information about the source of the data
708          */
709         public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
710         {
711                 // Decide whether to load or append
712                 if (_track.getNumPoints() > 0)
713                 {
714                         // ask whether to replace or append
715                         int answer = 0;
716                         if (_autoAppendNextFile) {
717                                 // Automatically append the next file
718                                 answer = JOptionPane.YES_OPTION;
719                         }
720                         else {
721                                 // Ask whether to append or not
722                                 answer = JOptionPane.showConfirmDialog(_frame,
723                                         I18nManager.getText("dialog.openappend.text"),
724                                         I18nManager.getText("dialog.openappend.title"),
725                                         JOptionPane.YES_NO_CANCEL_OPTION);
726                         }
727                         _autoAppendNextFile = false; // reset flag to cancel autoappend
728
729                         if (answer == JOptionPane.YES_OPTION)
730                         {
731                                 // append data to current Track
732                                 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
733                                 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
734                                 _undoStack.add(undo);
735                                 _track.combine(inLoadedTrack);
736                                 // set source information
737                                 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
738                                 _trackInfo.getFileInfo().addSource(inSourceInfo);
739                         }
740                         else if (answer == JOptionPane.NO_OPTION)
741                         {
742                                 // Don't append, replace data
743                                 PhotoList photos = null;
744                                 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
745                                         photos = _trackInfo.getPhotoList().cloneList();
746                                 }
747                                 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
748                                 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
749                                 _undoStack.add(undo);
750                                 _lastSavePosition = _undoStack.size();
751                                 _trackInfo.getSelection().clearAll();
752                                 _track.load(inLoadedTrack);
753                                 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
754                                 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
755                                 _trackInfo.getPhotoList().removeCorrelatedPhotos();
756                                 _trackInfo.getAudioList().removeCorrelatedAudios();
757                         }
758                 }
759                 else
760                 {
761                         // Currently no data held, so transfer received data
762                         UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
763                         undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
764                         _undoStack.add(undo);
765                         _lastSavePosition = _undoStack.size();
766                         _trackInfo.getSelection().clearAll();
767                         _track.load(inLoadedTrack);
768                         inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
769                         _trackInfo.getFileInfo().addSource(inSourceInfo);
770                 }
771                 // Update config before subscribers are told
772                 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
773                 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
774                 UpdateMessageBroker.informSubscribers();
775                 // Update status bar
776                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
777                         + " '" + inSourceInfo.getName() + "'");
778                 // update menu
779                 _menuManager.informFileLoaded();
780                 // recentre viewport on new file data
781                 _viewport.recentreViewport();
782                 // update main window title
783                 updateTitle();
784                 // Remove busy lock
785                 _busyLoading = false;
786                 // load next file if there's a queue
787                 loadNextFile();
788         }
789
790         /**
791          * Inform the app that NO data was loaded, eg cancel pressed
792          * Only needed if there's another file waiting in the queue
793          */
794         public void informNoDataLoaded()
795         {
796                 // Load next file if there's a queue
797                 loadNextFile();
798         }
799
800         /**
801          * External trigger to automatically append the next loaded file
802          * instead of prompting to replace or append
803          */
804         public void autoAppendNextFile()
805         {
806                 _autoAppendNextFile = true;
807         }
808
809         /**
810          * Load the next file in the waiting list, if any
811          */
812         private void loadNextFile()
813         {
814                 if (_dataFiles == null || _dataFiles.size() == 0) {
815                         _dataFiles = null;
816                 }
817                 else {
818                         new Thread(new Runnable() {
819                                 public void run() {
820                                         File f = _dataFiles.get(0);
821                                         _dataFiles.remove(0);
822                                         _autoAppendNextFile = true;
823                                         _fileLoader.openFile(f);
824                                 }
825                         }).start();
826                 }
827         }
828
829
830         /**
831          * Accept a list of loaded photos
832          * @param inPhotoSet Set of Photo objects
833          */
834         public void informPhotosLoaded(Set<Photo> inPhotoSet)
835         {
836                 if (inPhotoSet != null && !inPhotoSet.isEmpty())
837                 {
838                         int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
839                         int numPhotosAdded = numsAdded[0];
840                         int numPointsAdded = numsAdded[1];
841                         if (numPhotosAdded > 0)
842                         {
843                                 // Save numbers so load can be undone
844                                 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
845                         }
846                         if (numPhotosAdded == 1) {
847                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
848                         }
849                         else {
850                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
851                         }
852                         // MAYBE: Improve message when photo(s) fail to load (eg already added)
853                         UpdateMessageBroker.informSubscribers();
854                         // update menu
855                         if (numPointsAdded > 0) _menuManager.informFileLoaded();
856                 }
857         }
858
859
860         /**
861          * Save the coordinates of photos in their exif data
862          */
863         public void saveExif()
864         {
865                 ExifSaver saver = new ExifSaver(_frame);
866                 saver.saveExifInformation(_trackInfo.getPhotoList());
867         }
868
869
870         /**
871          * Inform the app that the data has been saved
872          */
873         public void informDataSaved()
874         {
875                 _lastSavePosition = _undoStack.size();
876         }
877
878
879         /**
880          * Begin undo process
881          */
882         public void beginUndo()
883         {
884                 if (_undoStack.isEmpty())
885                 {
886                         // Nothing to undo
887                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
888                                 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
889                 }
890                 else
891                 {
892                         new UndoManager(this, _frame).show();
893                 }
894         }
895
896
897         /**
898          * Clear the undo stack (losing all undo information
899          */
900         public void clearUndo()
901         {
902                 // Exit if nothing to undo
903                 if (_undoStack == null || _undoStack.isEmpty())
904                         return;
905                 // Has track got unsaved data?
906                 boolean unsaved = hasDataUnsaved();
907                 // Confirm operation with dialog
908                 int answer = JOptionPane.showConfirmDialog(_frame,
909                         I18nManager.getText("dialog.clearundo.text"),
910                         I18nManager.getText("dialog.clearundo.title"),
911                         JOptionPane.YES_NO_OPTION);
912                 if (answer == JOptionPane.YES_OPTION)
913                 {
914                         _undoStack.clear();
915                         _lastSavePosition = 0;
916                         if (unsaved) _lastSavePosition = -1;
917                         UpdateMessageBroker.informSubscribers();
918                 }
919         }
920
921
922         /**
923          * Undo the specified number of actions
924          * @param inNumUndos number of actions to undo
925          */
926         public void undoActions(int inNumUndos)
927         {
928                 try
929                 {
930                         for (int i=0; i<inNumUndos; i++)
931                         {
932                                 _undoStack.popOperation().performUndo(_trackInfo);
933                         }
934                         String message = "" + inNumUndos + " "
935                                  + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
936                         UpdateMessageBroker.informSubscribers(message);
937                 }
938                 catch (UndoException ue)
939                 {
940                         showErrorMessageNoLookup("error.undofailed.title",
941                                 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
942                         _undoStack.clear();
943                 }
944                 catch (EmptyStackException empty) {}
945                 UpdateMessageBroker.informSubscribers();
946         }
947
948         /**
949          * @return the current data status, used for later comparison
950          */
951         public DataStatus getCurrentDataStatus()
952         {
953                 return new DataStatus(_undoStack.size(), _undoStack.getNumUndos());
954         }
955
956
957         /**
958          * Display a standard error message
959          * @param inTitleKey key to lookup for window title
960          * @param inMessageKey key to lookup for error message
961          */
962         public void showErrorMessage(String inTitleKey, String inMessageKey)
963         {
964                 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
965                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
966         }
967
968         /**
969          * Display a standard error message
970          * @param inTitleKey key to lookup for window title
971          * @param inMessage error message
972          */
973         public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
974         {
975                 JOptionPane.showMessageDialog(_frame, inMessage,
976                         I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
977         }
978
979         /**
980          * @param inViewport viewport object
981          */
982         public void setViewport(Viewport inViewport)
983         {
984                 _viewport = inViewport;
985         }
986
987         /**
988          * @return current viewport object
989          */
990         public Viewport getViewport()
991         {
992                 return _viewport;
993         }
994
995         /**
996          * Set the controller for the full screen mode
997          * @param inController controller object
998          */
999         public void setSidebarController(SidebarController inController)
1000         {
1001                 _sidebarController = inController;
1002         }
1003
1004         /**
1005          * Toggle sidebars on and off
1006          */
1007         public void toggleSidebars()
1008         {
1009                 _sidebarController.toggle();
1010         }
1011
1012         /** @return true if App is currently busy with loading data */
1013         public boolean isBusyLoading() {
1014                 return _busyLoading;
1015         }
1016
1017         /** @return current app mode */
1018         public AppMode getCurrentMode() {
1019                 return _appMode;
1020         }
1021
1022         /** @param inMode the current app mode */
1023         public void setCurrentMode(AppMode inMode) {
1024                 _appMode = inMode;
1025         }
1026
1027         /** Update main window title **/
1028         public void updateTitle() {
1029                 ArrayList<String> filenames = _trackInfo.getFileInfo().getFilenames();
1030                 if (filenames.size() > 0) {
1031                         _frame.setTitle(_titlePrefix + ": " + String.join(", ", filenames));
1032                 }
1033                 else
1034                 {
1035                         _frame.setTitle(_titlePrefix);
1036                 }
1037         }
1038 }