]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/App.java
Version 6, October 2008
[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.browser.BrowserLauncher;
11 import tim.prune.browser.UrlGenerator;
12 import tim.prune.correlate.PhotoCorrelator;
13 import tim.prune.correlate.PointPair;
14 import tim.prune.data.Coordinate;
15 import tim.prune.data.DataPoint;
16 import tim.prune.data.Field;
17 import tim.prune.data.LatLonRectangle;
18 import tim.prune.data.Latitude;
19 import tim.prune.data.Longitude;
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.edit.FieldEditList;
25 import tim.prune.edit.PointEditor;
26 import tim.prune.edit.PointNameEditor;
27 import tim.prune.gui.MenuManager;
28 import tim.prune.gui.TimeOffsetDialog;
29 import tim.prune.gui.UndoManager;
30 import tim.prune.load.FileLoader;
31 import tim.prune.load.GpsLoader;
32 import tim.prune.load.JpegLoader;
33 import tim.prune.save.ExifSaver;
34 import tim.prune.save.FileSaver;
35 import tim.prune.save.GpxExporter;
36 import tim.prune.save.KmlExporter;
37 import tim.prune.save.PovExporter;
38 import tim.prune.threedee.ThreeDException;
39 import tim.prune.threedee.ThreeDWindow;
40 import tim.prune.threedee.WindowFactory;
41 import tim.prune.undo.UndoAddTimeOffset;
42 import tim.prune.undo.UndoCompress;
43 import tim.prune.undo.UndoConnectPhoto;
44 import tim.prune.undo.UndoConnectPhotoWithClone;
45 import tim.prune.undo.UndoCorrelatePhotos;
46 import tim.prune.undo.UndoCreatePoint;
47 import tim.prune.undo.UndoCutAndMove;
48 import tim.prune.undo.UndoDeleteDuplicates;
49 import tim.prune.undo.UndoDeletePhoto;
50 import tim.prune.undo.UndoDeletePoint;
51 import tim.prune.undo.UndoDeleteRange;
52 import tim.prune.undo.UndoDisconnectPhoto;
53 import tim.prune.undo.UndoEditPoint;
54 import tim.prune.undo.UndoException;
55 import tim.prune.undo.UndoInsert;
56 import tim.prune.undo.UndoLoad;
57 import tim.prune.undo.UndoLoadPhotos;
58 import tim.prune.undo.UndoMergeTrackSegments;
59 import tim.prune.undo.UndoOperation;
60 import tim.prune.undo.UndoRearrangeWaypoints;
61 import tim.prune.undo.UndoReverseSection;
62
63
64 /**
65  * Main controller for the application
66  */
67 public class App
68 {
69         // Instance variables
70         private JFrame _frame = null;
71         private Track _track = null;
72         private TrackInfo _trackInfo = null;
73         private int _lastSavePosition = 0;
74         private MenuManager _menuManager = null;
75         private FileLoader _fileLoader = null;
76         private JpegLoader _jpegLoader = null;
77         private GpsLoader _gpsLoader = null;
78         private FileSaver _fileSaver = null;
79         private KmlExporter _kmlExporter = null;
80         private GpxExporter _gpxExporter = null;
81         private PovExporter _povExporter = null;
82         private BrowserLauncher _browserLauncher = null;
83         private Stack _undoStack = null;
84         private boolean _mangleTimestampsConfirmed = false;
85
86         // Constants
87         public static final int REARRANGE_TO_START   = 0;
88         public static final int REARRANGE_TO_END     = 1;
89         public static final int REARRANGE_TO_NEAREST = 2;
90
91
92         /**
93          * Constructor
94          * @param inFrame frame object for application
95          */
96         public App(JFrame inFrame)
97         {
98                 _frame = inFrame;
99                 _undoStack = new Stack();
100                 _track = new Track();
101                 _trackInfo = new TrackInfo(_track);
102         }
103
104
105         /**
106          * @return the current TrackInfo
107          */
108         public TrackInfo getTrackInfo()
109         {
110                 return _trackInfo;
111         }
112
113         /**
114          * Check if the application has unsaved data
115          * @return true if data is unsaved, false otherwise
116          */
117         public boolean hasDataUnsaved()
118         {
119                 return (_undoStack.size() > _lastSavePosition
120                         && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
121         }
122
123         /**
124          * @return the undo stack
125          */
126         public Stack getUndoStack()
127         {
128                 return _undoStack;
129         }
130
131         /**
132          * Set the MenuManager object to be informed about changes
133          * @param inManager MenuManager object
134          */
135         public void setMenuManager(MenuManager inManager)
136         {
137                 _menuManager = inManager;
138         }
139
140
141         /**
142          * Open a file containing track or waypoint data
143          */
144         public void openFile()
145         {
146                 if (_fileLoader == null)
147                         _fileLoader = new FileLoader(this, _frame);
148                 _fileLoader.openFile();
149         }
150
151
152         /**
153          * Add a photo or a directory of photos
154          */
155         public void addPhotos()
156         {
157                 if (_jpegLoader == null)
158                         _jpegLoader = new JpegLoader(this, _frame);
159                 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
160         }
161
162         /**
163          * Start a load from Gps
164          */
165         public void beginLoadFromGps()
166         {
167                 if (_gpsLoader == null)
168                         _gpsLoader = new GpsLoader(this, _frame);
169                 _gpsLoader.openDialog();
170         }
171
172         /**
173          * Save the file in the selected format
174          */
175         public void saveFile()
176         {
177                 if (_track == null)
178                 {
179                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
180                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
181                 }
182                 else
183                 {
184                         if (_fileSaver == null) {
185                                 _fileSaver = new FileSaver(this, _frame, _track);
186                         }
187                         char delim = ',';
188                         if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
189                         _fileSaver.showDialog(delim);
190                 }
191         }
192
193
194         /**
195          * Export track data as Kml
196          */
197         public void exportKml()
198         {
199                 if (_track == null)
200                 {
201                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
202                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
203                 }
204                 else
205                 {
206                         // Invoke the export
207                         if (_kmlExporter == null)
208                         {
209                                 _kmlExporter = new KmlExporter(_frame, _trackInfo);
210                         }
211                         _kmlExporter.showDialog();
212                 }
213         }
214
215
216         /**
217          * Export track data as Gpx
218          */
219         public void exportGpx()
220         {
221                 if (_track == null)
222                 {
223                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
224                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
225                 }
226                 else
227                 {
228                         // Invoke the export
229                         if (_gpxExporter == null)
230                         {
231                                 _gpxExporter = new GpxExporter(_frame, _trackInfo);
232                         }
233                         _gpxExporter.showDialog();
234                 }
235         }
236
237
238         /**
239          * Export track data as Pov without specifying settings
240          */
241         public void exportPov()
242         {
243                 exportPov(false, 0.0, 0.0, 0.0, 0);
244         }
245
246         /**
247          * Export track data as Pov and also specify settings
248          * @param inX X component of unit vector
249          * @param inY Y component of unit vector
250          * @param inZ Z component of unit vector
251          * @param inAltitudeCap altitude cap
252          */
253         public void exportPov(double inX, double inY, double inZ, int inAltitudeCap)
254         {
255                 exportPov(true, inX, inY, inZ, inAltitudeCap);
256         }
257
258         /**
259          * Export track data as Pov with optional angle specification
260          * @param inDefineAngles true to define angles, false to ignore
261          * @param inX X component of unit vector
262          * @param inY Y component of unit vector
263          * @param inZ Z component of unit vector
264          * @param inAltitudeCap altitude cap
265          */
266         private void exportPov(boolean inDefineSettings, double inX, double inY, double inZ, int inAltitudeCap)
267         {
268                 // Check track has data to export
269                 if (_track == null || _track.getNumPoints() <= 0)
270                 {
271                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
272                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
273                 }
274                 else
275                 {
276                         // Make new exporter if necessary
277                         if (_povExporter == null)
278                         {
279                                 _povExporter = new PovExporter(_frame, _track);
280                         }
281                         // Specify angles if necessary
282                         if (inDefineSettings)
283                         {
284                                 _povExporter.setCameraCoordinates(inX, inY, inZ);
285                                 _povExporter.setAltitudeCap(inAltitudeCap);
286                         }
287                         // Initiate export
288                         _povExporter.showDialog();
289                 }
290         }
291
292
293         /**
294          * Exit the application if confirmed
295          */
296         public void exit()
297         {
298                 // grab focus
299                 _frame.toFront();
300                 _frame.requestFocus();
301                 // check if ok to exit
302                 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
303                 if (!hasDataUnsaved()
304                         || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
305                                 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
306                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
307                         == JOptionPane.YES_OPTION)
308                 {
309                         System.exit(0);
310                 }
311         }
312
313
314         /**
315          * Edit the currently selected point
316          */
317         public void editCurrentPoint()
318         {
319                 if (_track != null)
320                 {
321                         DataPoint currentPoint = _trackInfo.getCurrentPoint();
322                         if (currentPoint != null)
323                         {
324                                 // Open point dialog to display details
325                                 PointEditor editor = new PointEditor(this, _frame);
326                                 editor.showDialog(_track, currentPoint);
327                         }
328                 }
329         }
330
331
332         /**
333          * Complete the point edit
334          * @param inEditList field values to edit
335          * @param inUndoList field values before edit
336          */
337         public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
338         {
339                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
340                 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
341                 {
342                         // add information to undo stack
343                         UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
344                         // pass to track for completion
345                         if (_track.editPoint(currentPoint, inEditList))
346                         {
347                                 _undoStack.push(undo);
348                                 // Confirm point edit
349                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
350                         }
351                 }
352         }
353
354
355         /**
356          * Edit the name of the currently selected (way)point
357          */
358         public void editCurrentPointName()
359         {
360                 if (_track != null)
361                 {
362                         DataPoint currentPoint = _trackInfo.getCurrentPoint();
363                         if (currentPoint != null)
364                         {
365                                 // Open point dialog to display details
366                                 PointNameEditor editor = new PointNameEditor(this, _frame);
367                                 editor.showDialog(currentPoint);
368                         }
369                 }
370         }
371
372
373         /**
374          * Delete the currently selected point
375          */
376         public void deleteCurrentPoint()
377         {
378                 if (_track == null) {return;}
379                 DataPoint currentPoint = _trackInfo.getCurrentPoint();
380                 if (currentPoint != null)
381                 {
382                         boolean deletePhoto = false;
383                         Photo currentPhoto = currentPoint.getPhoto();
384                         if (currentPhoto != null)
385                         {
386                                 // Confirm deletion of photo or decoupling
387                                 int response = JOptionPane.showConfirmDialog(_frame,
388                                         I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
389                                         I18nManager.getText("dialog.deletepoint.title"),
390                                         JOptionPane.YES_NO_CANCEL_OPTION);
391                                 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
392                                 {
393                                         // cancel pressed- abort delete
394                                         return;
395                                 }
396                                 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
397                         }
398                         // store necessary information to undo it later
399                         int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
400                         int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
401                         DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
402                         // Construct Undo object
403                         UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
404                                 nextTrackPoint != null && nextTrackPoint.getSegmentStart());
405                         // call track to delete point
406                         if (_trackInfo.deletePoint())
407                         {
408                                 // Delete was successful so add undo info to stack
409                                 _undoStack.push(undo);
410                                 if (currentPhoto != null)
411                                 {
412                                         // delete photo if necessary
413                                         if (deletePhoto)
414                                         {
415                                                 _trackInfo.getPhotoList().deletePhoto(photoIndex);
416                                         }
417                                         else
418                                         {
419                                                 // decouple photo from point
420                                                 currentPhoto.setDataPoint(null);
421                                         }
422                                 }
423                                 // Confirm
424                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
425                         }
426                 }
427         }
428
429
430         /**
431          * Delete the currently selected range
432          */
433         public void deleteSelectedRange()
434         {
435                 if (_track != null)
436                 {
437                         // Find out if photos should be deleted or not
438                         int selStart = _trackInfo.getSelection().getStart();
439                         int selEnd = _trackInfo.getSelection().getEnd();
440                         if (selStart >= 0 && selEnd >= selStart)
441                         {
442                                 int numToDelete = selEnd - selStart + 1;
443                                 boolean[] deletePhotos = new boolean[numToDelete];
444                                 Photo[] photosToDelete = new Photo[numToDelete];
445                                 boolean deleteAll = false;
446                                 boolean deleteNone = false;
447                                 String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
448                                         I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
449                                         I18nManager.getText("button.cancel")};
450                                 DataPoint point = null;
451                                 for (int i=0; i<numToDelete; i++)
452                                 {
453                                         point = _trackInfo.getTrack().getPoint(i + selStart);
454                                         if (point != null && point.getPhoto() != null)
455                                         {
456                                                 if (deleteAll)
457                                                 {
458                                                         deletePhotos[i] = true;
459                                                         photosToDelete[i] = point.getPhoto();
460                                                 }
461                                                 else if (deleteNone) {deletePhotos[i] = false;}
462                                                 else
463                                                 {
464                                                         int response = JOptionPane.showOptionDialog(_frame,
465                                                                 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
466                                                                 I18nManager.getText("dialog.deletepoint.title"),
467                                                                 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
468                                                                 questionOptions, questionOptions[1]);
469                                                         // check for cancel or close
470                                                         if (response == 4 || response == -1) {return;}
471                                                         // check for yes or yes to all
472                                                         if (response == 0 || response == 2)
473                                                         {
474                                                                 deletePhotos[i] = true;
475                                                                 photosToDelete[i] = point.getPhoto();
476                                                                 if (response == 2) {deleteAll = true;}
477                                                         }
478                                                         // check for no to all
479                                                         if (response == 3) {deleteNone = true;}
480                                                 }
481                                         }
482                                 }
483                                 // add information to undo stack
484                                 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
485                                 // delete requested photos
486                                 for (int i=0; i<numToDelete; i++)
487                                 {
488                                         point = _trackInfo.getTrack().getPoint(i + selStart);
489                                         if (point != null && point.getPhoto() != null)
490                                         {
491                                                 if (deletePhotos[i])
492                                                 {
493                                                         // delete photo from list
494                                                         _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
495                                                 }
496                                                 else
497                                                 {
498                                                         // decouple from point
499                                                         point.getPhoto().setDataPoint(null);
500                                                 }
501                                         }
502                                 }
503                                 // call track to delete range
504                                 if (_trackInfo.deleteRange())
505                                 {
506                                         _undoStack.push(undo);
507                                         // Confirm
508                                         UpdateMessageBroker.informSubscribers("" + numToDelete + " "
509                                                 + I18nManager.getText("confirm.deletepoint.multi"));
510                                 }
511                         }
512                 }
513         }
514
515
516         /**
517          * Delete all the duplicate points in the track
518          */
519         public void deleteDuplicates()
520         {
521                 if (_track != null)
522                 {
523                         // Save undo information
524                         UndoOperation undo = new UndoDeleteDuplicates(_track);
525                         // tell track to do it
526                         int numDeleted = _trackInfo.deleteDuplicates();
527                         if (numDeleted > 0)
528                         {
529                                 _undoStack.add(undo);
530                                 String message = null;
531                                 if (numDeleted == 1)
532                                 {
533                                         message = "1 " + I18nManager.getText("confirm.deleteduplicates.single");
534                                 }
535                                 else
536                                 {
537                                         message = "" + numDeleted + " " + I18nManager.getText("confirm.deleteduplicates.multi");
538                                 }
539                                 // Pass message to broker
540                                 UpdateMessageBroker.informSubscribers(message);
541                         }
542                         else
543                         {
544                                 // No duplicates found to delete
545                                 JOptionPane.showMessageDialog(_frame,
546                                         I18nManager.getText("dialog.deleteduplicates.nonefound"),
547                                         I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE);
548                         }
549                 }
550         }
551
552
553         /**
554          * Compress the track
555          */
556         public void compressTrack()
557         {
558                 UndoCompress undo = new UndoCompress(_track);
559                 // Get compression parameter
560                 Object compParam = JOptionPane.showInputDialog(_frame,
561                         I18nManager.getText("dialog.compresstrack.parameter.text"),
562                         I18nManager.getText("dialog.compresstrack.title"),
563                         JOptionPane.QUESTION_MESSAGE, null, null, "100");
564                 int compNumber = parseNumber(compParam);
565                 if (compNumber <= 0) return;
566                 // call track to do compress
567                 int numPointsDeleted = _trackInfo.compress(compNumber);
568                 // add to undo stack if successful
569                 if (numPointsDeleted > 0)
570                 {
571                         undo.setNumPointsDeleted(numPointsDeleted);
572                         _undoStack.add(undo);
573                         UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
574                                  + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
575                 }
576                 else
577                 {
578                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.compresstrack.nonefound"),
579                                 I18nManager.getText("dialog.compresstrack.title"), JOptionPane.WARNING_MESSAGE);
580                 }
581         }
582
583
584         /**
585          * Reverse the currently selected section of the track
586          */
587         public void reverseRange()
588         {
589                 // check whether Timestamp field exists, and if so confirm reversal
590                 int selStart = _trackInfo.getSelection().getStart();
591                 int selEnd = _trackInfo.getSelection().getEnd();
592                 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
593                         || _mangleTimestampsConfirmed
594                         || (JOptionPane.showConfirmDialog(_frame,
595                                  I18nManager.getText("dialog.confirmreversetrack.text"),
596                                  I18nManager.getText("dialog.confirmreversetrack.title"),
597                                  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
598                 {
599                         UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
600                         // call track to reverse range
601                         if (_track.reverseRange(selStart, selEnd))
602                         {
603                                 _undoStack.add(undo);
604                                 // Confirm
605                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
606                         }
607                 }
608         }
609
610         /**
611          * Trigger the dialog to add a time offset to the current selection
612          */
613         public void beginAddTimeOffset()
614         {
615                 int selStart = _trackInfo.getSelection().getStart();
616                 int selEnd = _trackInfo.getSelection().getEnd();
617                 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)) {
618                         JOptionPane.showMessageDialog(_frame,
619                                 I18nManager.getText("dialog.addtimeoffset.notimestamps"),
620                                 I18nManager.getText("dialog.addtimeoffset.title"), JOptionPane.ERROR_MESSAGE);
621                 }
622                 else {
623                         TimeOffsetDialog timeDialog = new TimeOffsetDialog(this, _frame);
624                         timeDialog.showDialog();
625                 }
626         }
627
628         /**
629          * Complete the add time offset function with the specified offset
630          * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
631          */
632         public void finishAddTimeOffset(long inTimeOffset)
633         {
634                 // Construct undo information
635                 int selStart = _trackInfo.getSelection().getStart();
636                 int selEnd = _trackInfo.getSelection().getEnd();
637                 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
638                 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset))
639                 {
640                         _undoStack.add(undo);
641                         UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
642                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
643                 }
644         }
645
646
647         /**
648          * Merge the track segments within the current selection
649          */
650         public void mergeTrackSegments()
651         {
652                 if (_trackInfo.getSelection().hasRangeSelected())
653                 {
654                         // Maybe could check segment start flags to see if it's worth merging
655                         // If first track point is already start and no other seg starts then do nothing
656
657                         int selStart = _trackInfo.getSelection().getStart();
658                         int selEnd = _trackInfo.getSelection().getEnd();
659                         // Make undo object
660                         UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
661                         // Call track to merge segments
662                         if (_track.mergeTrackSegments(selStart, selEnd)) {
663                                 _undoStack.add(undo);
664                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
665                         }
666                 }
667         }
668
669
670         /**
671          * Interpolate the two selected points
672          */
673         public void interpolateSelection()
674         {
675                 // Get number of points to add
676                 Object numPointsStr = JOptionPane.showInputDialog(_frame,
677                         I18nManager.getText("dialog.interpolate.parameter.text"),
678                         I18nManager.getText("dialog.interpolate.title"),
679                         JOptionPane.QUESTION_MESSAGE, null, null, "");
680                 int numPoints = parseNumber(numPointsStr);
681                 if (numPoints <= 0) return;
682
683                 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
684                         numPoints);
685                 // call track to interpolate
686                 if (_trackInfo.interpolate(numPoints))
687                 {
688                         _undoStack.add(undo);
689                 }
690         }
691
692
693         /**
694          * Create a new point at the given lat/long coordinates
695          * @param inLat latitude
696          * @param inLong longitude
697          */
698         public void createPoint(double inLat, double inLong)
699         {
700                 // create undo object
701                 UndoCreatePoint undo = new UndoCreatePoint();
702                 // create point and add to track
703                 DataPoint point = new DataPoint(new Latitude(inLat, Coordinate.FORMAT_NONE), new Longitude(inLong, Coordinate.FORMAT_NONE), null);
704                 point.setSegmentStart(true);
705                 _track.appendPoints(new DataPoint[] {point});
706                 _trackInfo.getSelection().selectPoint(_trackInfo.getTrack().getNumPoints()-1);
707                 // add undo object to stack
708                 _undoStack.add(undo);
709                 // update listeners
710                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
711         }
712
713
714         /**
715          * Rearrange the waypoints into track order
716          * @param inFunction nearest point, all to end or all to start
717          */
718         public void rearrangeWaypoints(int inFunction)
719         {
720                 UndoRearrangeWaypoints undo = new UndoRearrangeWaypoints(_track);
721                 boolean success = false;
722                 if (inFunction == REARRANGE_TO_START || inFunction == REARRANGE_TO_END)
723                 {
724                         // Collect the waypoints to the start or end of the track
725                         success = _track.collectWaypoints(inFunction == REARRANGE_TO_START);
726                 }
727                 else
728                 {
729                         // Interleave the waypoints into track order
730                         success = _track.interleaveWaypoints();
731                 }
732                 if (success)
733                 {
734                         _undoStack.add(undo);
735                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.rearrangewaypoints"));
736                 }
737                 else
738                 {
739                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.rearrange.noop"),
740                                 I18nManager.getText("error.function.noop.title"), JOptionPane.WARNING_MESSAGE);
741                 }
742         }
743
744
745         /**
746          * Cut the current selection and move it to before the currently selected point
747          */
748         public void cutAndMoveSelection()
749         {
750                 int startIndex = _trackInfo.getSelection().getStart();
751                 int endIndex = _trackInfo.getSelection().getEnd();
752                 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
753                 // If timestamps would be mangled by cut/move, confirm
754                 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
755                         || _mangleTimestampsConfirmed
756                         || (JOptionPane.showConfirmDialog(_frame,
757                                  I18nManager.getText("dialog.confirmcutandmove.text"),
758                                  I18nManager.getText("dialog.confirmcutandmove.title"),
759                                  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
760                 {
761                         // Find points to set segment flags
762                         DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
763                         DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
764                         DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
765                         // Make undo object
766                         UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
767                         // Call track info to move track section
768                         if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
769                         {
770                                 // Set segment start flags (first track point, next track point, move to point)
771                                 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
772                                 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
773                                 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
774
775                                 // Add undo object to stack, set confirm message
776                                 _undoStack.add(undo);
777                                 _trackInfo.getSelection().deselectRange();
778                                 UpdateMessageBroker.informSubscribers();
779                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
780                         }
781                 }
782         }
783
784
785         /**
786          * Open a new window with the 3d view
787          */
788         public void show3dWindow()
789         {
790                 ThreeDWindow window = WindowFactory.getWindow(this, _frame);
791                 if (window == null)
792                 {
793                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.function.nojava3d"),
794                                 I18nManager.getText("error.function.notavailable.title"), JOptionPane.WARNING_MESSAGE);
795                 }
796                 else
797                 {
798                         try
799                         {
800                                 // Pass the track object and show the window
801                                 window.setTrack(_track);
802                                 window.show();
803                         }
804                         catch (ThreeDException e)
805                         {
806                                 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.3d") + ": " + e.getMessage(),
807                                         I18nManager.getText("error.3d.title"), JOptionPane.ERROR_MESSAGE);
808                         }
809                 }
810         }
811
812
813         /**
814          * Select all points
815          */
816         public void selectAll()
817         {
818                 _trackInfo.getSelection().select(0, 0, _track.getNumPoints()-1);
819         }
820
821         /**
822          * Select nothing
823          */
824         public void selectNone()
825         {
826                 // deselect point, range and photo
827                 _trackInfo.getSelection().clearAll();
828         }
829
830
831         /**
832          * Receive loaded data and optionally merge with current Track
833          * @param inFieldArray array of fields
834          * @param inDataArray array of data
835          * @param inAltFormat altitude format
836          * @param inFilename filename used
837          */
838         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
839         {
840                 informDataLoaded(inFieldArray, inDataArray, inAltFormat, inFilename, false);
841         }
842
843         /**
844          * Receive loaded data and optionally merge with current Track
845          * @param inFieldArray array of fields
846          * @param inDataArray array of data
847          * @param inAltFormat altitude format
848          * @param inFilename filename used
849          * @param inOverrideAppend true to override append question and always append
850          */
851         public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat,
852                 String inFilename, boolean inOverrideAppend)
853         {
854                 // Check whether loaded array can be properly parsed into a Track
855                 Track loadedTrack = new Track();
856                 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
857                 if (loadedTrack.getNumPoints() <= 0)
858                 {
859                         JOptionPane.showMessageDialog(_frame,
860                                 I18nManager.getText("error.load.nopoints"),
861                                 I18nManager.getText("error.load.dialogtitle"),
862                                 JOptionPane.ERROR_MESSAGE);
863                         return;
864                 }
865                 // Decide whether to load or append
866                 if (_track != null && _track.getNumPoints() > 0)
867                 {
868                         // ask whether to replace or append
869                         int answer = JOptionPane.YES_OPTION;
870                         if (!inOverrideAppend) {
871                                 answer = JOptionPane.showConfirmDialog(_frame,
872                                         I18nManager.getText("dialog.openappend.text"),
873                                         I18nManager.getText("dialog.openappend.title"),
874                                         JOptionPane.YES_NO_CANCEL_OPTION);
875                         }
876                         if (answer == JOptionPane.YES_OPTION)
877                         {
878                                 // append data to current Track
879                                 _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
880                                 _track.combine(loadedTrack);
881                                 // set filename if currently empty
882                                 if (_trackInfo.getFileInfo().getNumFiles() == 0)
883                                 {
884                                         _trackInfo.getFileInfo().setFile(inFilename);
885                                 }
886                                 else
887                                 {
888                                         _trackInfo.getFileInfo().addFile();
889                                 }
890                         }
891                         else if (answer == JOptionPane.NO_OPTION)
892                         {
893                                 // Don't append, replace data
894                                 PhotoList photos = null;
895                                 if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
896                                 {
897                                         photos = _trackInfo.getPhotoList().cloneList();
898                                 }
899                                 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
900                                 _lastSavePosition = _undoStack.size();
901                                 // TODO: Should be possible to reuse the Track object already loaded?
902                                 _trackInfo.selectPoint(null);
903                                 _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
904                                 _trackInfo.getFileInfo().setFile(inFilename);
905                                 if (photos != null)
906                                 {
907                                         _trackInfo.getPhotoList().removeCorrelatedPhotos();
908                                 }
909                         }
910                 }
911                 else
912                 {
913                         // currently no data held, so use received data
914                         _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
915                         _lastSavePosition = _undoStack.size();
916                         _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
917                         _trackInfo.getFileInfo().setFile(inFilename);
918                 }
919                 UpdateMessageBroker.informSubscribers();
920                 // Update status bar
921                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile") + " '" + inFilename + "'");
922                 // update menu
923                 _menuManager.informFileLoaded();
924         }
925
926
927         /**
928          * Accept a list of loaded photos
929          * @param inPhotoSet Set of Photo objects
930          */
931         public void informPhotosLoaded(Set inPhotoSet)
932         {
933                 if (inPhotoSet != null && !inPhotoSet.isEmpty())
934                 {
935                         int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
936                         int numPhotosAdded = numsAdded[0];
937                         int numPointsAdded = numsAdded[1];
938                         if (numPhotosAdded > 0)
939                         {
940                                 // Save numbers so load can be undone
941                                 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
942                         }
943                         if (numPhotosAdded == 1)
944                         {
945                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
946                         }
947                         else
948                         {
949                                 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
950                         }
951                         // TODO: Improve message when photo(s) fail to load (eg already added)
952                         UpdateMessageBroker.informSubscribers();
953                         // update menu
954                         _menuManager.informFileLoaded();
955                 }
956         }
957
958
959         /**
960          * Connect the current photo to the current point
961          */
962         public void connectPhotoToPoint()
963         {
964                 Photo photo = _trackInfo.getCurrentPhoto();
965                 DataPoint point = _trackInfo.getCurrentPoint();
966                 if (photo != null && point != null)
967                 {
968                         if (point.getPhoto() != null)
969                         {
970                                 // point already has a photo, confirm cloning of new point
971                                 if (JOptionPane.showConfirmDialog(_frame,
972                                         I18nManager.getText("dialog.connectphoto.clonepoint"),
973                                         I18nManager.getText("dialog.connect.title"),
974                                         JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION)
975                                 {
976                                         // Create undo, clone point and attach
977                                         int pointIndex = _trackInfo.getSelection().getCurrentPointIndex() + 1;
978                                         // insert new point after current one
979                                         point = point.clonePoint();
980                                         UndoConnectPhotoWithClone undo = new UndoConnectPhotoWithClone(
981                                                 point, photo.getFile().getName(), pointIndex);
982                                         _track.insertPoint(point, pointIndex);
983                                         photo.setDataPoint(point);
984                                         point.setPhoto(photo);
985                                         _undoStack.add(undo);
986                                         UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
987                                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
988                                 }
989                         }
990                         else
991                         {
992                                 // point doesn't currently have a photo, so just connect it
993                                 _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
994                                 photo.setDataPoint(point);
995                                 point.setPhoto(photo);
996                                 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
997                                 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
998                         }
999                 }
1000         }
1001
1002
1003         /**
1004          * Disconnect the current photo from its point
1005          */
1006         public void disconnectPhotoFromPoint()
1007         {
1008                 Photo photo = _trackInfo.getCurrentPhoto();
1009                 if (photo != null && photo.getDataPoint() != null)
1010                 {
1011                         DataPoint point = photo.getDataPoint();
1012                         _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
1013                         // disconnect
1014                         photo.setDataPoint(null);
1015                         point.setPhoto(null);
1016                         UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
1017                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
1018                 }
1019         }
1020
1021
1022         /**
1023          * Remove the current photo, if any
1024          */
1025         public void deleteCurrentPhoto()
1026         {
1027                 // Delete the current photo, and optionally its point too, keeping undo information
1028                 Photo currentPhoto = _trackInfo.getCurrentPhoto();
1029                 if (currentPhoto != null)
1030                 {
1031                         // Photo is selected, see if it has a point or not
1032                         boolean photoDeleted = false;
1033                         UndoDeletePhoto undoAction = null;
1034                         if (currentPhoto.getDataPoint() == null)
1035                         {
1036                                 // no point attached, so just delete photo
1037                                 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
1038                                         null, -1);
1039                                 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
1040                         }
1041                         else
1042                         {
1043                                 // point is attached, so need to confirm point deletion
1044                                 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
1045                                         currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
1046                                 int response = JOptionPane.showConfirmDialog(_frame,
1047                                         I18nManager.getText("dialog.deletephoto.deletepoint"),
1048                                         I18nManager.getText("dialog.deletephoto.title"),
1049                                         JOptionPane.YES_NO_CANCEL_OPTION);
1050                                 boolean deletePointToo = (response == JOptionPane.YES_OPTION);
1051                                 // Cancel delete if cancel pressed or dialog closed
1052                                 if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
1053                                 {
1054                                         photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
1055                                 }
1056                         }
1057                         // Add undo information to stack if necessary
1058                         if (photoDeleted)
1059                         {
1060                                 _undoStack.add(undoAction);
1061                         }
1062                 }
1063         }
1064
1065
1066         /**
1067          * Begin the photo correlation process by invoking dialog
1068          */
1069         public void beginCorrelatePhotos()
1070         {
1071                 PhotoCorrelator correlator = new PhotoCorrelator(this, _frame);
1072                 // TODO: Do we need to keep a reference to this Photo Correlator object to reuse it later?
1073                 correlator.begin();
1074         }
1075
1076
1077         /**
1078          * Finish the photo correlation process
1079          * @param inPointPairs array of PointPair objects describing operation
1080          */
1081         public void finishCorrelatePhotos(PointPair[] inPointPairs)
1082         {
1083                 // TODO: This method is too big for App, but where should it go?
1084                 if (inPointPairs != null && inPointPairs.length > 0)
1085                 {
1086                         // begin to construct undo information
1087                         UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_trackInfo);
1088                         // loop over Photos
1089                         int arraySize = inPointPairs.length;
1090                         int i = 0, numPhotos = 0;
1091                         int numPointsToCreate = 0;
1092                         PointPair pair = null;
1093                         for (i=0; i<arraySize; i++)
1094                         {
1095                                 pair = inPointPairs[i];
1096                                 if (pair != null && pair.isValid())
1097                                 {
1098                                         if (pair.getMinSeconds() == 0L)
1099                                         {
1100                                                 // exact match
1101                                                 Photo pointPhoto = pair.getPointBefore().getPhoto();
1102                                                 if (pointPhoto == null)
1103                                                 {
1104                                                         // photo coincides with photoless point so connect the two
1105                                                         pair.getPointBefore().setPhoto(pair.getPhoto());
1106                                                         pair.getPhoto().setDataPoint(pair.getPointBefore());
1107                                                 }
1108                                                 else if (pointPhoto.equals(pair.getPhoto()))
1109                                                 {
1110                                                         // photo is already connected, nothing to do
1111                                                 }
1112                                                 else
1113                                                 {
1114                                                         // point is already connected to a different photo, so need to clone point
1115                                                         numPointsToCreate++;
1116                                                 }
1117                                         }
1118                                         else
1119                                         {
1120                                                 // photo time falls between two points, so need to interpolate new one
1121                                                 numPointsToCreate++;
1122                                         }
1123                                         numPhotos++;
1124                                 }
1125                         }
1126                         // Second loop, to create points if necessary
1127                         if (numPointsToCreate > 0)
1128                         {
1129                                 // make new array for added points
1130                                 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
1131                                 int pointNum = 0;
1132                                 DataPoint pointToAdd = null;
1133                                 for (i=0; i<arraySize; i++)
1134                                 {
1135                                         pair = inPointPairs[i];
1136                                         if (pair != null && pair.isValid())
1137                                         {
1138                                                 pointToAdd = null;
1139                                                 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
1140                                                  && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
1141                                                 {
1142                                                         // clone point
1143                                                         pointToAdd = pair.getPointBefore().clonePoint();
1144                                                 }
1145                                                 else if (pair.getMinSeconds() > 0L)
1146                                                 {
1147                                                         // interpolate point
1148                                                         pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
1149                                                 }
1150                                                 if (pointToAdd != null)
1151                                                 {
1152                                                         // link photo to point
1153                                                         pointToAdd.setPhoto(pair.getPhoto());
1154                                                         pair.getPhoto().setDataPoint(pointToAdd);
1155                                                         // set to start of segment so not joined in track
1156                                                         pointToAdd.setSegmentStart(true);
1157                                                         // add to point array
1158                                                         addedPoints[pointNum] = pointToAdd;
1159                                                         pointNum++;
1160                                                 }
1161                                         }
1162                                 }
1163                                 // expand track
1164                                 _track.appendPoints(addedPoints);
1165                         }
1166                         // add undo information to stack
1167                         undo.setNumPhotosCorrelated(numPhotos);
1168                         _undoStack.add(undo);
1169                         // confirm correlation
1170                         UpdateMessageBroker.informSubscribers("" + numPhotos + " "
1171                                  + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi")));
1172                         // observers already informed by track update
1173                 }
1174         }
1175
1176
1177         /**
1178          * Save the coordinates of photos in their exif data
1179          */
1180         public void saveExif()
1181         {
1182                 ExifSaver saver = new ExifSaver(_frame);
1183                 saver.saveExifInformation(_trackInfo.getPhotoList());
1184         }
1185
1186
1187         /**
1188          * Inform the app that the data has been saved
1189          */
1190         public void informDataSaved()
1191         {
1192                 _lastSavePosition = _undoStack.size();
1193         }
1194
1195
1196         /**
1197          * Begin undo process
1198          */
1199         public void beginUndo()
1200         {
1201                 if (_undoStack.isEmpty())
1202                 {
1203                         // Nothing to undo
1204                         JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
1205                                 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
1206                 }
1207                 else
1208                 {
1209                         new UndoManager(this, _frame);
1210                 }
1211         }
1212
1213
1214         /**
1215          * Clear the undo stack (losing all undo information
1216          */
1217         public void clearUndo()
1218         {
1219                 // Exit if nothing to undo
1220                 if (_undoStack == null || _undoStack.isEmpty())
1221                         return;
1222                 // Has track got unsaved data?
1223                 boolean unsaved = hasDataUnsaved();
1224                 // Confirm operation with dialog
1225                 int answer = JOptionPane.showConfirmDialog(_frame,
1226                         I18nManager.getText("dialog.clearundo.text"),
1227                         I18nManager.getText("dialog.clearundo.title"),
1228                         JOptionPane.YES_NO_OPTION);
1229                 if (answer == JOptionPane.YES_OPTION)
1230                 {
1231                         _undoStack.clear();
1232                         _lastSavePosition = 0;
1233                         if (unsaved) _lastSavePosition = -1;
1234                         UpdateMessageBroker.informSubscribers();
1235                 }
1236         }
1237
1238
1239         /**
1240          * Undo the specified number of actions
1241          * @param inNumUndos number of actions to undo
1242          */
1243         public void undoActions(int inNumUndos)
1244         {
1245                 try
1246                 {
1247                         for (int i=0; i<inNumUndos; i++)
1248                         {
1249                                 ((UndoOperation) _undoStack.pop()).performUndo(_trackInfo);
1250                         }
1251                         String message = "" + inNumUndos + " "
1252                                  + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
1253                         UpdateMessageBroker.informSubscribers(message);
1254                 }
1255                 catch (UndoException ue)
1256                 {
1257                         JOptionPane.showMessageDialog(_frame,
1258                                 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage(),
1259                                 I18nManager.getText("error.undofailed.title"),
1260                                 JOptionPane.ERROR_MESSAGE);
1261                         _undoStack.clear();
1262                         UpdateMessageBroker.informSubscribers();
1263                 }
1264                 catch (EmptyStackException empty) {}
1265         }
1266
1267
1268         /**
1269          * Helper method to parse an Object into an integer
1270          * @param inObject object, eg from dialog
1271          * @return int value given
1272          */
1273         private static int parseNumber(Object inObject)
1274         {
1275                 int num = 0;
1276                 if (inObject != null)
1277                 {
1278                         try
1279                         {
1280                                 num = Integer.parseInt(inObject.toString());
1281                         }
1282                         catch (NumberFormatException nfe)
1283                         {}
1284                 }
1285                 return num;
1286         }
1287
1288         /**
1289          * Show a brief help message
1290          */
1291         public void showHelp()
1292         {
1293                 // show the dialog and offer to open home page
1294                 Object[] buttonTexts = {I18nManager.getText("button.showwebpage"), I18nManager.getText("button.cancel")};
1295                 if (JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.help.help"),
1296                                 I18nManager.getText("menu.help"), JOptionPane.YES_NO_OPTION,
1297                                 JOptionPane.INFORMATION_MESSAGE, null, buttonTexts, buttonTexts[1])
1298                         == JOptionPane.YES_OPTION)
1299                 {
1300                         // User selected to launch home page
1301                         if (_browserLauncher == null) {_browserLauncher = new BrowserLauncher();}
1302                         _browserLauncher.launchBrowser("http://activityworkshop.net/software/prune/index.html");
1303                 }
1304         }
1305
1306         /**
1307          * Show a map url in an external browser
1308          */
1309         public void showExternalMap(int inSourceIndex)
1310         {
1311                 if (_browserLauncher == null) {_browserLauncher = new BrowserLauncher();}
1312                 _browserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
1313         }
1314 }