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