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