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