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