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