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