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