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