3 import java.util.ArrayList;
4 import java.util.EmptyStackException;
6 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.Coordinate;
14 import tim.prune.data.DataPoint;
15 import tim.prune.data.Field;
16 import tim.prune.data.LatLonRectangle;
17 import tim.prune.data.Latitude;
18 import tim.prune.data.Longitude;
19 import tim.prune.data.NumberUtils;
20 import tim.prune.data.Photo;
21 import tim.prune.data.PhotoList;
22 import tim.prune.data.Track;
23 import tim.prune.data.TrackInfo;
24 import tim.prune.function.browser.BrowserLauncher;
25 import tim.prune.function.browser.UrlGenerator;
26 import tim.prune.function.edit.FieldEditList;
27 import tim.prune.function.edit.PointEditor;
28 import tim.prune.gui.MenuManager;
29 import tim.prune.gui.UndoManager;
30 import tim.prune.gui.Viewport;
31 import tim.prune.load.FileLoader;
32 import tim.prune.load.JpegLoader;
33 import tim.prune.save.ExifSaver;
34 import tim.prune.save.FileSaver;
35 import tim.prune.undo.*;
39 * Main controller for the application
44 private JFrame _frame = null;
45 private Track _track = null;
46 private TrackInfo _trackInfo = null;
47 private int _lastSavePosition = 0;
48 private MenuManager _menuManager = null;
49 private FileLoader _fileLoader = null;
50 private JpegLoader _jpegLoader = null;
51 private FileSaver _fileSaver = null;
52 private Stack<UndoOperation> _undoStack = null;
53 private boolean _mangleTimestampsConfirmed = false;
54 private Viewport _viewport = null;
55 private ArrayList<File> _dataFiles = null;
56 private boolean _firstDataFile = true;
61 * @param inFrame frame object for application
63 public App(JFrame inFrame)
66 _undoStack = new Stack<UndoOperation>();
68 _trackInfo = new TrackInfo(_track);
69 FunctionLibrary.initialise(this);
74 * @return the current TrackInfo
76 public TrackInfo getTrackInfo()
82 * @return the dialog frame
84 public JFrame getFrame()
90 * Check if the application has unsaved data
91 * @return true if data is unsaved, false otherwise
93 public boolean hasDataUnsaved()
95 return (_undoStack.size() > _lastSavePosition
96 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
100 * @return the undo stack
102 public Stack<UndoOperation> getUndoStack()
108 * Load the specified data files one by one
109 * @param inDataFiles arraylist containing File objects to load
111 public void loadDataFiles(ArrayList<File> inDataFiles)
113 if (inDataFiles == null || inDataFiles.size() == 0) {
117 _dataFiles = inDataFiles;
118 File f = _dataFiles.get(0);
119 _dataFiles.remove(0);
120 // Start load of specified file
121 if (_fileLoader == null)
122 _fileLoader = new FileLoader(this, _frame);
123 _firstDataFile = true;
124 _fileLoader.openFile(f);
129 * Complete a function execution
130 * @param inUndo undo object to be added to stack
131 * @param inConfirmText confirmation text
133 public void completeFunction(UndoOperation inUndo, String inConfirmText)
135 _undoStack.add(inUndo);
136 UpdateMessageBroker.informSubscribers(inConfirmText);
140 * Set the MenuManager object to be informed about changes
141 * @param inManager MenuManager object
143 public void setMenuManager(MenuManager inManager)
145 _menuManager = inManager;
150 * Open a file containing track or waypoint data
152 public void openFile()
154 if (_fileLoader == null)
155 _fileLoader = new FileLoader(this, _frame);
156 _fileLoader.openFile();
161 * Add a photo or a directory of photos
163 public void addPhotos()
165 if (_jpegLoader == null)
166 _jpegLoader = new JpegLoader(this, _frame);
167 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
171 * Save the file in the selected format
173 public void saveFile()
175 if (_track == null) {
176 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
180 if (_fileSaver == null) {
181 _fileSaver = new FileSaver(this, _frame, _track);
184 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
185 _fileSaver.showDialog(delim);
191 * Exit the application if confirmed
197 _frame.requestFocus();
198 // check if ok to exit
199 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
200 if (!hasDataUnsaved()
201 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
202 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
203 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
204 == JOptionPane.YES_OPTION)
212 * Edit the currently selected point
214 public void editCurrentPoint()
218 DataPoint currentPoint = _trackInfo.getCurrentPoint();
219 if (currentPoint != null)
221 // Open point dialog to display details
222 PointEditor editor = new PointEditor(this, _frame);
223 editor.showDialog(_track, currentPoint);
230 * Complete the point edit
231 * @param inEditList field values to edit
232 * @param inUndoList field values before edit
234 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
236 DataPoint currentPoint = _trackInfo.getCurrentPoint();
237 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
239 // add information to undo stack
240 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
241 // pass to track for completion
242 if (_track.editPoint(currentPoint, inEditList))
244 _undoStack.push(undo);
245 // Confirm point edit
246 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
253 * Delete the currently selected point
255 public void deleteCurrentPoint()
257 if (_track == null) {return;}
258 DataPoint currentPoint = _trackInfo.getCurrentPoint();
259 if (currentPoint != null)
261 boolean deletePhoto = false;
262 Photo currentPhoto = currentPoint.getPhoto();
263 if (currentPhoto != null)
265 // Confirm deletion of photo or decoupling
266 int response = JOptionPane.showConfirmDialog(_frame,
267 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
268 I18nManager.getText("dialog.deletepoint.title"),
269 JOptionPane.YES_NO_CANCEL_OPTION);
270 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
272 // cancel pressed- abort delete
275 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
277 // store necessary information to undo it later
278 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
279 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
280 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
281 // Construct Undo object
282 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
283 nextTrackPoint != null && nextTrackPoint.getSegmentStart());
284 // call track to delete point
285 if (_trackInfo.deletePoint())
287 // Delete was successful so add undo info to stack
288 _undoStack.push(undo);
289 if (currentPhoto != null)
291 // delete photo if necessary
294 _trackInfo.getPhotoList().deletePhoto(photoIndex);
298 // decouple photo from point
299 currentPhoto.setDataPoint(null);
303 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
310 * Delete the currently selected range
312 public void deleteSelectedRange()
316 // Find out if photos should be deleted or not
317 int selStart = _trackInfo.getSelection().getStart();
318 int selEnd = _trackInfo.getSelection().getEnd();
319 if (selStart >= 0 && selEnd >= selStart)
321 int numToDelete = selEnd - selStart + 1;
322 boolean[] deletePhotos = new boolean[numToDelete];
323 Photo[] photosToDelete = new Photo[numToDelete];
324 boolean deleteAll = false;
325 boolean deleteNone = false;
326 String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
327 I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
328 I18nManager.getText("button.cancel")};
329 DataPoint point = null;
330 for (int i=0; i<numToDelete; i++)
332 point = _trackInfo.getTrack().getPoint(i + selStart);
333 if (point != null && point.getPhoto() != null)
337 deletePhotos[i] = true;
338 photosToDelete[i] = point.getPhoto();
340 else if (deleteNone) {deletePhotos[i] = false;}
343 int response = JOptionPane.showOptionDialog(_frame,
344 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
345 I18nManager.getText("dialog.deletepoint.title"),
346 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
347 questionOptions, questionOptions[1]);
348 // check for cancel or close
349 if (response == 4 || response == -1) {return;}
350 // check for yes or yes to all
351 if (response == 0 || response == 2)
353 deletePhotos[i] = true;
354 photosToDelete[i] = point.getPhoto();
355 if (response == 2) {deleteAll = true;}
357 // check for no to all
358 if (response == 3) {deleteNone = true;}
362 // add information to undo stack
363 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
364 // delete requested photos
365 for (int i=0; i<numToDelete; i++)
367 point = _trackInfo.getTrack().getPoint(i + selStart);
368 if (point != null && point.getPhoto() != null)
372 // delete photo from list
373 _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
377 // decouple from point
378 point.getPhoto().setDataPoint(null);
382 // call track to delete range
383 if (_trackInfo.deleteRange())
385 _undoStack.push(undo);
387 UpdateMessageBroker.informSubscribers("" + numToDelete + " "
388 + I18nManager.getText("confirm.deletepoint.multi"));
396 * Finish the compression by deleting the marked points
398 public void finishCompressTrack()
400 UndoCompress undo = new UndoCompress(_track);
401 // call track to do compress
402 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
403 // add to undo stack if successful
404 if (numPointsDeleted > 0)
406 undo.setNumPointsDeleted(numPointsDeleted);
407 _undoStack.add(undo);
408 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
409 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
412 showErrorMessage("function.compress", "dialog.compress.nonefound");
417 * Reverse the currently selected section of the track
419 public void reverseRange()
421 // check whether Timestamp field exists, and if so confirm reversal
422 int selStart = _trackInfo.getSelection().getStart();
423 int selEnd = _trackInfo.getSelection().getEnd();
424 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
425 || _mangleTimestampsConfirmed
426 || (JOptionPane.showConfirmDialog(_frame,
427 I18nManager.getText("dialog.confirmreversetrack.text"),
428 I18nManager.getText("dialog.confirmreversetrack.title"),
429 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
431 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
432 // call track to reverse range
433 if (_track.reverseRange(selStart, selEnd))
435 _undoStack.add(undo);
437 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
443 * Complete the add time offset function with the specified offset
444 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
446 public void finishAddTimeOffset(long inTimeOffset)
448 // Construct undo information
449 int selStart = _trackInfo.getSelection().getStart();
450 int selEnd = _trackInfo.getSelection().getEnd();
451 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
452 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset))
454 _undoStack.add(undo);
455 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
456 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
462 * Complete the add altitude offset function with the specified offset
463 * @param inOffset altitude offset to add as String
464 * @param inFormat altitude format of offset (eg Feet, Metres)
466 public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
469 if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
472 // Construct undo information
473 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
474 int selStart = _trackInfo.getSelection().getStart();
475 int selEnd = _trackInfo.getSelection().getEnd();
476 // How many decimal places are given in the offset?
477 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
478 boolean success = false;
479 // Decimal offset given
481 double offsetd = Double.parseDouble(inOffset);
482 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
484 catch (NumberFormatException nfe) {}
487 _undoStack.add(undo);
488 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
489 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
495 * Merge the track segments within the current selection
497 public void mergeTrackSegments()
499 if (_trackInfo.getSelection().hasRangeSelected())
501 // Maybe could check segment start flags to see if it's worth merging
502 // If first track point is already start and no other seg starts then do nothing
504 int selStart = _trackInfo.getSelection().getStart();
505 int selEnd = _trackInfo.getSelection().getEnd();
507 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
508 // Call track to merge segments
509 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
510 _undoStack.add(undo);
511 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
518 * Interpolate the two selected points
520 public void interpolateSelection()
522 // Get number of points to add
523 Object numPointsStr = JOptionPane.showInputDialog(_frame,
524 I18nManager.getText("dialog.interpolate.parameter.text"),
525 I18nManager.getText("dialog.interpolate.title"),
526 JOptionPane.QUESTION_MESSAGE, null, null, "");
527 int numPoints = parseNumber(numPointsStr);
528 if (numPoints <= 0) return;
530 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
532 // call track to interpolate
533 if (_trackInfo.interpolate(numPoints))
535 _undoStack.add(undo);
541 * Average the selected points
543 public void averageSelection()
545 // Find following track point
546 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
547 boolean segFlag = false;
548 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
549 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
550 // call track info object to do the averaging
551 if (_trackInfo.average())
553 _undoStack.add(undo);
559 * Create a new point at the given lat/long coordinates
560 * @param inLat latitude
561 * @param inLong longitude
563 public void createPoint(double inLat, double inLong)
565 // create undo object
566 UndoCreatePoint undo = new UndoCreatePoint();
567 // create point and add to track
568 DataPoint point = new DataPoint(new Latitude(inLat, Coordinate.FORMAT_NONE), new Longitude(inLong, Coordinate.FORMAT_NONE), null);
569 point.setSegmentStart(true);
570 _track.appendPoints(new DataPoint[] {point});
571 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
572 // add undo object to stack
573 _undoStack.add(undo);
575 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
580 * Cut the current selection and move it to before the currently selected point
582 public void cutAndMoveSelection()
584 int startIndex = _trackInfo.getSelection().getStart();
585 int endIndex = _trackInfo.getSelection().getEnd();
586 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
587 // If timestamps would be mangled by cut/move, confirm
588 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
589 || _mangleTimestampsConfirmed
590 || (JOptionPane.showConfirmDialog(_frame,
591 I18nManager.getText("dialog.confirmcutandmove.text"),
592 I18nManager.getText("dialog.confirmcutandmove.title"),
593 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
595 // Find points to set segment flags
596 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
597 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
598 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
600 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
601 // Call track info to move track section
602 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
604 // Set segment start flags (first track point, next track point, move to point)
605 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
606 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
607 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
609 // Add undo object to stack, set confirm message
610 _undoStack.add(undo);
611 _trackInfo.getSelection().selectRange(-1, -1);
612 UpdateMessageBroker.informSubscribers();
613 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
621 public void selectNone()
623 // deselect point, range and photo
624 _trackInfo.getSelection().clearAll();
625 _track.clearDeletionMarkers();
629 * Receive loaded data and optionally merge with current Track
630 * @param inFieldArray array of fields
631 * @param inDataArray array of data
632 * @param inAltFormat altitude format
633 * @param inFilename filename used
635 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, Altitude.Format inAltFormat,
638 // Check whether loaded array can be properly parsed into a Track
639 Track loadedTrack = new Track();
640 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
641 if (loadedTrack.getNumPoints() <= 0)
643 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
644 // load next file if there's a queue
648 // Decide whether to load or append
649 if (_track.getNumPoints() > 0)
651 // ask whether to replace or append
653 if (_dataFiles == null || _firstDataFile) {
654 answer = JOptionPane.showConfirmDialog(_frame,
655 I18nManager.getText("dialog.openappend.text"),
656 I18nManager.getText("dialog.openappend.title"),
657 JOptionPane.YES_NO_CANCEL_OPTION);
660 // Automatically append if there's a file load queue
661 answer = JOptionPane.YES_OPTION;
663 if (answer == JOptionPane.YES_OPTION)
665 // append data to current Track
666 _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
667 _track.combine(loadedTrack);
668 // set filename if currently empty
669 if (_trackInfo.getFileInfo().getNumFiles() == 0)
671 _trackInfo.getFileInfo().setFile(inFilename);
675 _trackInfo.getFileInfo().addFile();
678 else if (answer == JOptionPane.NO_OPTION)
680 // Don't append, replace data
681 PhotoList photos = null;
682 if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
684 photos = _trackInfo.getPhotoList().cloneList();
686 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
687 _lastSavePosition = _undoStack.size();
688 _trackInfo.getSelection().clearAll();
689 _track.load(loadedTrack);
690 _trackInfo.getFileInfo().setFile(inFilename);
693 _trackInfo.getPhotoList().removeCorrelatedPhotos();
699 // Currently no data held, so transfer received data
700 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
701 _lastSavePosition = _undoStack.size();
702 _trackInfo.getSelection().clearAll();
703 _track.load(loadedTrack);
704 _trackInfo.getFileInfo().setFile(inFilename);
706 UpdateMessageBroker.informSubscribers();
708 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile") + " '" + inFilename + "'");
710 _menuManager.informFileLoaded();
711 // load next file if there's a queue
716 * Inform the app that NO data was loaded, eg cancel pressed
717 * Only needed if there's another file waiting in the queue
719 public void informNoDataLoaded()
721 // Load next file if there's a queue
726 * Load the next file in the waiting list, if any
728 private void loadNextFile()
730 _firstDataFile = false;
731 if (_dataFiles == null || _dataFiles.size() == 0) {
735 new Thread(new Runnable() {
737 File f = _dataFiles.get(0);
738 _dataFiles.remove(0);
739 _fileLoader.openFile(f);
747 * Accept a list of loaded photos
748 * @param inPhotoSet Set of Photo objects
750 public void informPhotosLoaded(Set<Photo> inPhotoSet)
752 if (inPhotoSet != null && !inPhotoSet.isEmpty())
754 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
755 int numPhotosAdded = numsAdded[0];
756 int numPointsAdded = numsAdded[1];
757 if (numPhotosAdded > 0)
759 // Save numbers so load can be undone
760 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
762 if (numPhotosAdded == 1)
764 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
768 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
770 // MAYBE: Improve message when photo(s) fail to load (eg already added)
771 UpdateMessageBroker.informSubscribers();
773 _menuManager.informFileLoaded();
779 * Connect the current photo to the current point
781 public void connectPhotoToPoint()
783 Photo photo = _trackInfo.getCurrentPhoto();
784 DataPoint point = _trackInfo.getCurrentPoint();
785 if (photo != null && point != null)
787 if (point.getPhoto() != null)
789 // point already has a photo, confirm cloning of new point
790 if (JOptionPane.showConfirmDialog(_frame,
791 I18nManager.getText("dialog.connectphoto.clonepoint"),
792 I18nManager.getText("dialog.connect.title"),
793 JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION)
795 // Create undo, clone point and attach
796 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex() + 1;
797 // insert new point after current one
798 point = point.clonePoint();
799 UndoConnectPhotoWithClone undo = new UndoConnectPhotoWithClone(
800 point, photo.getFile().getName(), pointIndex);
801 _track.insertPoint(point, pointIndex);
802 photo.setDataPoint(point);
803 point.setPhoto(photo);
804 _undoStack.add(undo);
805 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
806 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
811 // point doesn't currently have a photo, so just connect it
812 _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
813 photo.setDataPoint(point);
814 point.setPhoto(photo);
815 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
816 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
823 * Disconnect the current photo from its point
825 public void disconnectPhotoFromPoint()
827 Photo photo = _trackInfo.getCurrentPhoto();
828 if (photo != null && photo.getDataPoint() != null)
830 DataPoint point = photo.getDataPoint();
831 _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
833 photo.setDataPoint(null);
834 point.setPhoto(null);
835 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
836 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
842 * Remove the current photo, if any
844 public void deleteCurrentPhoto()
846 // Delete the current photo, and optionally its point too, keeping undo information
847 Photo currentPhoto = _trackInfo.getCurrentPhoto();
848 if (currentPhoto != null)
850 // Photo is selected, see if it has a point or not
851 boolean photoDeleted = false;
852 UndoDeletePhoto undoAction = null;
853 if (currentPhoto.getDataPoint() == null)
855 // no point attached, so just delete photo
856 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
858 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
862 // point is attached, so need to confirm point deletion
863 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
864 currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
865 int response = JOptionPane.showConfirmDialog(_frame,
866 I18nManager.getText("dialog.deletephoto.deletepoint"),
867 I18nManager.getText("dialog.deletephoto.title"),
868 JOptionPane.YES_NO_CANCEL_OPTION);
869 boolean deletePointToo = (response == JOptionPane.YES_OPTION);
870 // Cancel delete if cancel pressed or dialog closed
871 if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
873 photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
876 // Add undo information to stack if necessary
879 _undoStack.add(undoAction);
886 * Save the coordinates of photos in their exif data
888 public void saveExif()
890 ExifSaver saver = new ExifSaver(_frame);
891 saver.saveExifInformation(_trackInfo.getPhotoList());
896 * Inform the app that the data has been saved
898 public void informDataSaved()
900 _lastSavePosition = _undoStack.size();
907 public void beginUndo()
909 if (_undoStack.isEmpty())
912 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
913 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
917 new UndoManager(this, _frame);
923 * Clear the undo stack (losing all undo information
925 public void clearUndo()
927 // Exit if nothing to undo
928 if (_undoStack == null || _undoStack.isEmpty())
930 // Has track got unsaved data?
931 boolean unsaved = hasDataUnsaved();
932 // Confirm operation with dialog
933 int answer = JOptionPane.showConfirmDialog(_frame,
934 I18nManager.getText("dialog.clearundo.text"),
935 I18nManager.getText("dialog.clearundo.title"),
936 JOptionPane.YES_NO_OPTION);
937 if (answer == JOptionPane.YES_OPTION)
940 _lastSavePosition = 0;
941 if (unsaved) _lastSavePosition = -1;
942 UpdateMessageBroker.informSubscribers();
948 * Undo the specified number of actions
949 * @param inNumUndos number of actions to undo
951 public void undoActions(int inNumUndos)
955 for (int i=0; i<inNumUndos; i++)
957 _undoStack.pop().performUndo(_trackInfo);
959 String message = "" + inNumUndos + " "
960 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
961 UpdateMessageBroker.informSubscribers(message);
963 catch (UndoException ue)
965 showErrorMessageNoLookup("error.undofailed.title",
966 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
968 UpdateMessageBroker.informSubscribers();
970 catch (EmptyStackException empty) {}
975 * Helper method to parse an Object into an integer
976 * @param inObject object, eg from dialog
977 * @return int value given
979 private static int parseNumber(Object inObject)
982 if (inObject != null)
986 num = Integer.parseInt(inObject.toString());
988 catch (NumberFormatException nfe)
995 * Show a map url in an external browser
996 * @param inSourceIndex index of map source to use
998 public void showExternalMap(int inSourceIndex)
1000 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
1004 * Display a standard error message
1005 * @param inTitleKey key to lookup for window title
1006 * @param inMessageKey key to lookup for error message
1008 public void showErrorMessage(String inTitleKey, String inMessageKey)
1010 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
1011 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1015 * Display a standard error message
1016 * @param inTitleKey key to lookup for window title
1017 * @param inMessage error message
1019 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
1021 JOptionPane.showMessageDialog(_frame, inMessage,
1022 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1026 * @param inViewport viewport object
1028 public void setViewport(Viewport inViewport)
1030 _viewport = inViewport;
1034 * @return current viewport object
1036 public Viewport getViewport()