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