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