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