4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
7 import java.util.Stack;
9 import javax.swing.JFrame;
10 import javax.swing.JOptionPane;
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;
55 * Main controller for the application
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;
77 * @param inFrame frame object for application
79 public App(JFrame inFrame)
82 _undoStack = new Stack<UndoOperation>();
84 _trackInfo = new TrackInfo(_track);
85 FunctionLibrary.initialise(this);
90 * @return the current TrackInfo
92 public TrackInfo getTrackInfo()
98 * @return the dialog frame
100 public JFrame getFrame()
106 * Check if the application has unsaved data
107 * @return true if data is unsaved, false otherwise
109 public boolean hasDataUnsaved()
111 return (_undoStack.size() > _lastSavePosition
112 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
116 * @return the undo stack
118 public Stack<UndoOperation> getUndoStack()
124 * Load the specified data files one by one
125 * @param inDataFiles arraylist containing File objects to load
127 public void loadDataFiles(ArrayList<File> inDataFiles)
129 if (inDataFiles == null || inDataFiles.size() == 0) {
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);
145 * Complete a function execution
146 * @param inUndo undo object to be added to stack
147 * @param inConfirmText confirmation text
149 public void completeFunction(UndoOperation inUndo, String inConfirmText)
151 _undoStack.add(inUndo);
152 UpdateMessageBroker.informSubscribers(inConfirmText);
156 * Set the MenuManager object to be informed about changes
157 * @param inManager MenuManager object
159 public void setMenuManager(MenuManager inManager)
161 _menuManager = inManager;
166 * Open a file containing track or waypoint data
168 public void openFile()
170 if (_fileLoader == null)
171 _fileLoader = new FileLoader(this, _frame);
172 _fileLoader.openFile();
177 * Add a photo or a directory of photos
179 public void addPhotos()
181 if (_jpegLoader == null)
182 _jpegLoader = new JpegLoader(this, _frame);
183 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
187 * Save the file in the selected format
189 public void saveFile()
191 if (_track == null) {
192 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
196 if (_fileSaver == null) {
197 _fileSaver = new FileSaver(this, _frame);
200 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
201 _fileSaver.showDialog(delim);
207 * Exit the application if confirmed
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)
228 * Edit the currently selected point
230 public void editCurrentPoint()
234 DataPoint currentPoint = _trackInfo.getCurrentPoint();
235 if (currentPoint != null)
237 // Open point dialog to display details
238 PointEditor editor = new PointEditor(this, _frame);
239 editor.showDialog(_track, currentPoint);
246 * Complete the point edit
247 * @param inEditList field values to edit
248 * @param inUndoList field values before edit
250 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
252 DataPoint currentPoint = _trackInfo.getCurrentPoint();
253 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
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))
260 _undoStack.push(undo);
261 // Confirm point edit
262 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
269 * Delete the currently selected point
271 public void deleteCurrentPoint()
273 if (_track == null) {return;}
274 DataPoint currentPoint = _trackInfo.getCurrentPoint();
275 if (currentPoint != null)
277 boolean deletePhoto = false;
278 Photo currentPhoto = currentPoint.getPhoto();
279 if (currentPhoto != null)
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)
288 // cancel pressed- abort delete
291 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
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())
303 // Delete was successful so add undo info to stack
304 _undoStack.push(undo);
305 if (currentPhoto != null)
307 // delete photo if necessary
310 _trackInfo.getPhotoList().deletePhoto(photoIndex);
314 // decouple photo from point
315 currentPhoto.setDataPoint(null);
319 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
326 * Delete the currently selected range
328 public void deleteSelectedRange()
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)
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++)
348 point = _trackInfo.getTrack().getPoint(i + selStart);
349 if (point != null && point.getPhoto() != null)
353 deletePhotos[i] = true;
354 photosToDelete[i] = point.getPhoto();
356 else if (deleteNone) {deletePhotos[i] = false;}
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)
369 deletePhotos[i] = true;
370 photosToDelete[i] = point.getPhoto();
371 if (response == 2) {deleteAll = true;}
373 // check for no to all
374 if (response == 3) {deleteNone = true;}
378 // add information to undo stack
379 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
380 // delete requested photos
381 for (int i=0; i<numToDelete; i++)
383 point = _trackInfo.getTrack().getPoint(i + selStart);
384 if (point != null && point.getPhoto() != null)
388 // delete photo from list
389 _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
393 // decouple from point
394 point.getPhoto().setDataPoint(null);
398 // call track to delete range
399 if (_trackInfo.deleteRange())
401 _undoStack.push(undo);
403 UpdateMessageBroker.informSubscribers("" + numToDelete + " "
404 + I18nManager.getText("confirm.deletepoint.multi"));
412 * Finish the compression by deleting the marked points
414 public void finishCompressTrack()
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)
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")));
428 showErrorMessage("function.compress", "dialog.compress.nonefound");
433 * Reverse the currently selected section of the track
435 public void reverseRange()
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)))
447 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
448 // call track to reverse range
449 if (_track.reverseRange(selStart, selEnd))
451 _undoStack.add(undo);
453 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
459 * Complete the add time offset function with the specified offset
460 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
462 public void finishAddTimeOffset(long inTimeOffset)
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))
470 _undoStack.add(undo);
471 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
472 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
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)
482 public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
485 if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
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
497 double offsetd = Double.parseDouble(inOffset);
498 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
500 catch (NumberFormatException nfe) {}
503 _undoStack.add(undo);
504 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
505 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
511 * Merge the track segments within the current selection
513 public void mergeTrackSegments()
515 if (_trackInfo.getSelection().hasRangeSelected())
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
520 int selStart = _trackInfo.getSelection().getStart();
521 int selEnd = _trackInfo.getSelection().getEnd();
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"));
534 * Interpolate the two selected points
536 public void interpolateSelection()
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;
546 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
548 // call track to interpolate
549 if (_trackInfo.interpolate(numPoints))
551 _undoStack.add(undo);
557 * Average the selected points
559 public void averageSelection()
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())
569 _undoStack.add(undo);
575 * Create a new point at the given position
576 * @param inPoint point to add
578 public void createPoint(DataPoint inPoint)
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);
590 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
595 * Cut the current selection and move it to before the currently selected point
597 public void cutAndMoveSelection()
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)))
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);
615 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
616 // Call track info to move track section
617 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
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);}
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"));
636 public void selectNone()
638 // deselect point, range and photo
639 _trackInfo.getSelection().clearAll();
640 _track.clearDeletionMarkers();
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
650 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
651 Altitude.Format inAltFormat, SourceInfo inSourceInfo)
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)
658 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
659 // load next file if there's a queue
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);
668 // Decide whether to load or append
669 if (_track.getNumPoints() > 0)
671 // ask whether to replace or append
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);
680 // Automatically append if there's a file load queue
681 answer = JOptionPane.YES_OPTION;
683 if (answer == JOptionPane.YES_OPTION)
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);
692 else if (answer == JOptionPane.NO_OPTION)
694 // Don't append, replace data
695 PhotoList photos = null;
696 if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
698 photos = _trackInfo.getPhotoList().cloneList();
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);
708 _trackInfo.getPhotoList().removeCorrelatedPhotos();
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);
722 UpdateMessageBroker.informSubscribers();
724 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
725 + " '" + inSourceInfo.getName() + "'");
727 _menuManager.informFileLoaded();
728 // load next file if there's a queue
733 * Inform the app that NO data was loaded, eg cancel pressed
734 * Only needed if there's another file waiting in the queue
736 public void informNoDataLoaded()
738 // Load next file if there's a queue
743 * Load the next file in the waiting list, if any
745 private void loadNextFile()
747 _firstDataFile = false;
748 if (_dataFiles == null || _dataFiles.size() == 0) {
752 new Thread(new Runnable() {
754 File f = _dataFiles.get(0);
755 _dataFiles.remove(0);
756 _fileLoader.openFile(f);
764 * Accept a list of loaded photos
765 * @param inPhotoSet Set of Photo objects
767 public void informPhotosLoaded(Set<Photo> inPhotoSet)
769 if (inPhotoSet != null && !inPhotoSet.isEmpty())
771 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
772 int numPhotosAdded = numsAdded[0];
773 int numPointsAdded = numsAdded[1];
774 if (numPhotosAdded > 0)
776 // Save numbers so load can be undone
777 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
779 if (numPhotosAdded == 1)
781 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
785 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
787 // MAYBE: Improve message when photo(s) fail to load (eg already added)
788 UpdateMessageBroker.informSubscribers();
790 _menuManager.informFileLoaded();
796 * Connect the current photo to the current point
798 public void connectPhotoToPoint()
800 Photo photo = _trackInfo.getCurrentPhoto();
801 DataPoint point = _trackInfo.getCurrentPoint();
802 if (photo != null && point != null)
804 if (point.getPhoto() == null)
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"));
818 * Disconnect the current photo from its point
820 public void disconnectPhotoFromPoint()
822 Photo photo = _trackInfo.getCurrentPhoto();
823 if (photo != null && photo.getDataPoint() != null)
825 DataPoint point = photo.getDataPoint();
826 _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
828 photo.setDataPoint(null);
829 point.setPhoto(null);
830 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
831 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
837 * Remove the current photo, if any
839 public void deleteCurrentPhoto()
841 // Delete the current photo, and optionally its point too, keeping undo information
842 Photo currentPhoto = _trackInfo.getCurrentPhoto();
843 if (currentPhoto != null)
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)
850 // no point attached, so just delete photo
851 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
853 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
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)
868 photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
871 // Add undo information to stack if necessary
874 _undoStack.add(undoAction);
881 * Save the coordinates of photos in their exif data
883 public void saveExif()
885 ExifSaver saver = new ExifSaver(_frame);
886 saver.saveExifInformation(_trackInfo.getPhotoList());
891 * Inform the app that the data has been saved
893 public void informDataSaved()
895 _lastSavePosition = _undoStack.size();
902 public void beginUndo()
904 if (_undoStack.isEmpty())
907 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
908 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
912 new UndoManager(this, _frame);
918 * Clear the undo stack (losing all undo information
920 public void clearUndo()
922 // Exit if nothing to undo
923 if (_undoStack == null || _undoStack.isEmpty())
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)
935 _lastSavePosition = 0;
936 if (unsaved) _lastSavePosition = -1;
937 UpdateMessageBroker.informSubscribers();
943 * Undo the specified number of actions
944 * @param inNumUndos number of actions to undo
946 public void undoActions(int inNumUndos)
950 for (int i=0; i<inNumUndos; i++)
952 _undoStack.pop().performUndo(_trackInfo);
954 String message = "" + inNumUndos + " "
955 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
956 UpdateMessageBroker.informSubscribers(message);
958 catch (UndoException ue)
960 showErrorMessageNoLookup("error.undofailed.title",
961 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
963 UpdateMessageBroker.informSubscribers();
965 catch (EmptyStackException empty) {}
970 * Helper method to parse an Object into an integer
971 * @param inObject object, eg from dialog
972 * @return int value given
974 private static int parseNumber(Object inObject)
977 if (inObject != null)
981 num = Integer.parseInt(inObject.toString());
983 catch (NumberFormatException nfe)
990 * Show a map url in an external browser
991 * @param inSourceIndex index of map source to use
993 public void showExternalMap(int inSourceIndex)
995 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
999 * Display a standard error message
1000 * @param inTitleKey key to lookup for window title
1001 * @param inMessageKey key to lookup for error message
1003 public void showErrorMessage(String inTitleKey, String inMessageKey)
1005 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
1006 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1010 * Display a standard error message
1011 * @param inTitleKey key to lookup for window title
1012 * @param inMessage error message
1014 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
1016 JOptionPane.showMessageDialog(_frame, inMessage,
1017 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1021 * @param inViewport viewport object
1023 public void setViewport(Viewport inViewport)
1025 _viewport = inViewport;
1029 * @return current viewport object
1031 public Viewport getViewport()