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