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