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