3 import java.util.EmptyStackException;
5 import java.util.Stack;
7 import javax.swing.JFrame;
8 import javax.swing.JOptionPane;
10 import tim.prune.browser.BrowserLauncher;
11 import tim.prune.browser.UrlGenerator;
12 import tim.prune.correlate.PhotoCorrelator;
13 import tim.prune.correlate.PointPair;
14 import tim.prune.data.DataPoint;
15 import tim.prune.data.Field;
16 import tim.prune.data.Photo;
17 import tim.prune.data.PhotoList;
18 import tim.prune.data.Track;
19 import tim.prune.data.TrackInfo;
20 import tim.prune.edit.FieldEditList;
21 import tim.prune.edit.PointEditor;
22 import tim.prune.edit.PointNameEditor;
23 import tim.prune.gui.MenuManager;
24 import tim.prune.gui.UndoManager;
25 import tim.prune.gui.map.MapWindow;
26 import tim.prune.load.FileLoader;
27 import tim.prune.load.JpegLoader;
28 import tim.prune.save.ExifSaver;
29 import tim.prune.save.FileSaver;
30 import tim.prune.save.GpxExporter;
31 import tim.prune.save.KmlExporter;
32 import tim.prune.save.PovExporter;
33 import tim.prune.threedee.ThreeDException;
34 import tim.prune.threedee.ThreeDWindow;
35 import tim.prune.threedee.WindowFactory;
36 import tim.prune.undo.UndoCompress;
37 import tim.prune.undo.UndoConnectPhoto;
38 import tim.prune.undo.UndoCorrelatePhotos;
39 import tim.prune.undo.UndoDeleteDuplicates;
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.UndoRearrangeWaypoints;
52 import tim.prune.undo.UndoReverseSection;
56 * Main controller for the application
61 private JFrame _frame = null;
62 private Track _track = null;
63 private TrackInfo _trackInfo = null;
64 private int _lastSavePosition = 0;
65 private MenuManager _menuManager = null;
66 private FileLoader _fileLoader = null;
67 private JpegLoader _jpegLoader = null;
68 private FileSaver _fileSaver = null;
69 private KmlExporter _kmlExporter = null;
70 private GpxExporter _gpxExporter = null;
71 private PovExporter _povExporter = null;
72 private BrowserLauncher _browserLauncher = null;
73 private Stack _undoStack = null;
74 private boolean _reversePointsConfirmed = false;
77 public static final int REARRANGE_TO_START = 0;
78 public static final int REARRANGE_TO_END = 1;
79 public static final int REARRANGE_TO_NEAREST = 2;
84 * @param inFrame frame object for application
86 public App(JFrame inFrame)
89 _undoStack = new Stack();
91 _trackInfo = new TrackInfo(_track);
96 * @return the current TrackInfo
98 public TrackInfo getTrackInfo()
104 * Check if the application has unsaved data
105 * @return true if data is unsaved, false otherwise
107 public boolean hasDataUnsaved()
109 return (_undoStack.size() > _lastSavePosition
110 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
114 * @return the undo stack
116 public Stack getUndoStack()
122 * Set the MenuManager object to be informed about changes
123 * @param inManager MenuManager object
125 public void setMenuManager(MenuManager inManager)
127 _menuManager = inManager;
132 * Open a file containing track or waypoint data
134 public void openFile()
136 if (_fileLoader == null)
137 _fileLoader = new FileLoader(this, _frame);
138 _fileLoader.openFile();
143 * Add a photo or a directory of photos
145 public void addPhotos()
147 if (_jpegLoader == null)
148 _jpegLoader = new JpegLoader(this, _frame);
149 _jpegLoader.openDialog();
154 * Save the file in the selected format
156 public void saveFile()
160 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
161 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
165 if (_fileSaver == null) {
166 _fileSaver = new FileSaver(this, _frame, _track);
168 _fileSaver.showDialog(_fileLoader.getLastUsedDelimiter());
174 * Export track data as Kml
176 public void exportKml()
180 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
181 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
186 if (_kmlExporter == null)
188 _kmlExporter = new KmlExporter(_frame, _trackInfo);
190 _kmlExporter.showDialog();
196 * Export track data as Gpx
198 public void exportGpx()
202 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
203 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
208 if (_gpxExporter == null)
210 _gpxExporter = new GpxExporter(_frame, _trackInfo);
212 _gpxExporter.showDialog();
218 * Export track data as Pov without specifying settings
220 public void exportPov()
222 exportPov(false, 0.0, 0.0, 0.0, 0);
226 * Export track data as Pov and also specify settings
227 * @param inX X component of unit vector
228 * @param inY Y component of unit vector
229 * @param inZ Z component of unit vector
230 * @param inAltitudeCap altitude cap
232 public void exportPov(double inX, double inY, double inZ, int inAltitudeCap)
234 exportPov(true, inX, inY, inZ, inAltitudeCap);
238 * Export track data as Pov with optional angle specification
239 * @param inDefineAngles true to define angles, false to ignore
240 * @param inX X component of unit vector
241 * @param inY Y component of unit vector
242 * @param inZ Z component of unit vector
243 * @param inAltitudeCap altitude cap
245 private void exportPov(boolean inDefineSettings, double inX, double inY, double inZ, int inAltitudeCap)
247 // Check track has data to export
248 if (_track == null || _track.getNumPoints() <= 0)
250 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
251 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
255 // Make new exporter if necessary
256 if (_povExporter == null)
258 _povExporter = new PovExporter(_frame, _track);
260 // Specify angles if necessary
261 if (inDefineSettings)
263 _povExporter.setCameraCoordinates(inX, inY, inZ);
264 _povExporter.setAltitudeCap(inAltitudeCap);
267 _povExporter.showDialog();
273 * Exit the application if confirmed
279 _frame.requestFocus();
280 // check if ok to exit
281 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
282 if (!hasDataUnsaved()
283 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
284 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
285 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
286 == JOptionPane.YES_OPTION)
294 * Edit the currently selected point
296 public void editCurrentPoint()
300 DataPoint currentPoint = _trackInfo.getCurrentPoint();
301 if (currentPoint != null)
303 // Open point dialog to display details
304 PointEditor editor = new PointEditor(this, _frame);
305 editor.showDialog(_track, currentPoint);
312 * Complete the point edit
313 * @param inEditList field values to edit
314 * @param inUndoList field values before edit
316 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
318 DataPoint currentPoint = _trackInfo.getCurrentPoint();
319 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
321 // add information to undo stack
322 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
323 // pass to track for completion
324 if (_track.editPoint(currentPoint, inEditList))
326 _undoStack.push(undo);
327 // Confirm point edit
328 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
335 * Edit the name of the currently selected (way)point
337 public void editCurrentPointName()
341 DataPoint currentPoint = _trackInfo.getCurrentPoint();
342 if (currentPoint != null)
344 // Open point dialog to display details
345 PointNameEditor editor = new PointNameEditor(this, _frame);
346 editor.showDialog(currentPoint);
353 * Delete the currently selected point
355 public void deleteCurrentPoint()
357 if (_track == null) {return;}
358 DataPoint currentPoint = _trackInfo.getCurrentPoint();
359 if (currentPoint != null)
361 boolean deletePhoto = false;
362 Photo currentPhoto = currentPoint.getPhoto();
363 if (currentPhoto != null)
365 // Confirm deletion of photo or decoupling
366 int response = JOptionPane.showConfirmDialog(_frame,
367 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
368 I18nManager.getText("dialog.deletepoint.title"),
369 JOptionPane.YES_NO_CANCEL_OPTION);
370 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
372 // cancel pressed- abort delete
375 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
377 // store necessary information to undo it later
378 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
379 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
380 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
381 // Construct Undo object
382 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
383 nextTrackPoint != null && nextTrackPoint.getSegmentStart());
384 // call track to delete point
385 if (_trackInfo.deletePoint())
387 // Delete was successful so add undo info to stack
388 _undoStack.push(undo);
389 if (currentPhoto != null)
391 // delete photo if necessary
394 _trackInfo.getPhotoList().deletePhoto(photoIndex);
398 // decouple photo from point
399 currentPhoto.setDataPoint(null);
403 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
410 * Delete the currently selected range
412 public void deleteSelectedRange()
416 // Find out if photos should be deleted or not
417 int selStart = _trackInfo.getSelection().getStart();
418 int selEnd = _trackInfo.getSelection().getEnd();
419 if (selStart >= 0 && selEnd >= selStart)
421 int numToDelete = selEnd - selStart + 1;
422 boolean[] deletePhotos = new boolean[numToDelete];
423 Photo[] photosToDelete = new Photo[numToDelete];
424 boolean deleteAll = false;
425 boolean deleteNone = false;
426 String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
427 I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
428 I18nManager.getText("button.cancel")};
429 DataPoint point = null;
430 for (int i=0; i<numToDelete; i++)
432 point = _trackInfo.getTrack().getPoint(i + selStart);
433 if (point != null && point.getPhoto() != null)
437 deletePhotos[i] = true;
438 photosToDelete[i] = point.getPhoto();
440 else if (deleteNone) {deletePhotos[i] = false;}
443 int response = JOptionPane.showOptionDialog(_frame,
444 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
445 I18nManager.getText("dialog.deletepoint.title"),
446 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
447 questionOptions, questionOptions[1]);
448 // check for cancel or close
449 if (response == 4 || response == -1) {return;}
450 // check for yes or yes to all
451 if (response == 0 || response == 2)
453 deletePhotos[i] = true;
454 photosToDelete[i] = point.getPhoto();
455 if (response == 2) {deleteAll = true;}
457 // check for no to all
458 if (response == 3) {deleteNone = true;}
462 // add information to undo stack
463 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
464 // delete requested photos
465 for (int i=0; i<numToDelete; i++)
467 point = _trackInfo.getTrack().getPoint(i + selStart);
468 if (point != null && point.getPhoto() != null)
472 // delete photo from list
473 _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
477 // decouple from point
478 point.getPhoto().setDataPoint(null);
482 // call track to delete range
483 if (_trackInfo.deleteRange())
485 _undoStack.push(undo);
487 UpdateMessageBroker.informSubscribers("" + numToDelete + " "
488 + I18nManager.getText("confirm.deletepoint.multi"));
496 * Delete all the duplicate points in the track
498 public void deleteDuplicates()
502 // Save undo information
503 UndoOperation undo = new UndoDeleteDuplicates(_track);
504 // tell track to do it
505 int numDeleted = _trackInfo.deleteDuplicates();
508 _undoStack.add(undo);
509 String message = null;
512 message = "1 " + I18nManager.getText("confirm.deleteduplicates.single");
516 message = "" + numDeleted + " " + I18nManager.getText("confirm.deleteduplicates.multi");
518 // Pass message to broker
519 UpdateMessageBroker.informSubscribers(message);
523 // No duplicates found to delete
524 JOptionPane.showMessageDialog(_frame,
525 I18nManager.getText("dialog.deleteduplicates.nonefound"),
526 I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE);
535 public void compressTrack()
537 UndoCompress undo = new UndoCompress(_track);
538 // Get compression parameter
539 Object compParam = JOptionPane.showInputDialog(_frame,
540 I18nManager.getText("dialog.compresstrack.parameter.text"),
541 I18nManager.getText("dialog.compresstrack.title"),
542 JOptionPane.QUESTION_MESSAGE, null, null, "100");
543 int compNumber = parseNumber(compParam);
544 if (compNumber <= 0) return;
545 // call track to do compress
546 int numPointsDeleted = _trackInfo.compress(compNumber);
547 // add to undo stack if successful
548 if (numPointsDeleted > 0)
550 undo.setNumPointsDeleted(numPointsDeleted);
551 _undoStack.add(undo);
552 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
553 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
557 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.compresstrack.nonefound"),
558 I18nManager.getText("dialog.compresstrack.title"), JOptionPane.WARNING_MESSAGE);
564 * Reverse the currently selected section of the track
566 public void reverseRange()
568 // check whether Timestamp field exists, and if so confirm reversal
569 int selStart = _trackInfo.getSelection().getStart();
570 int selEnd = _trackInfo.getSelection().getEnd();
571 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
572 || _reversePointsConfirmed
573 || (JOptionPane.showConfirmDialog(_frame,
574 I18nManager.getText("dialog.confirmreversetrack.text"),
575 I18nManager.getText("dialog.confirmreversetrack.title"),
576 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_reversePointsConfirmed = true)))
578 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
579 // call track to reverse range
580 if (_track.reverseRange(selStart, selEnd))
582 _undoStack.add(undo);
584 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
590 * Merge the track segments within the current selection
592 public void mergeTrackSegments()
594 if (_trackInfo.getSelection().hasRangeSelected())
596 // Maybe could check segment start flags to see if it's worth merging
597 // If first track point is already start and no other seg starts then do nothing
599 int selStart = _trackInfo.getSelection().getStart();
600 int selEnd = _trackInfo.getSelection().getEnd();
602 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
603 // Call track to merge segments
604 if (_track.mergeTrackSegments(selStart, selEnd)) {
605 _undoStack.add(undo);
606 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
613 * Interpolate the two selected points
615 public void interpolateSelection()
617 // Get number of points to add
618 Object numPointsStr = JOptionPane.showInputDialog(_frame,
619 I18nManager.getText("dialog.interpolate.parameter.text"),
620 I18nManager.getText("dialog.interpolate.title"),
621 JOptionPane.QUESTION_MESSAGE, null, null, "");
622 int numPoints = parseNumber(numPointsStr);
623 if (numPoints <= 0) return;
625 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
627 // call track to interpolate
628 if (_trackInfo.interpolate(numPoints))
630 _undoStack.add(undo);
636 * Rearrange the waypoints into track order
637 * @param inFunction nearest point, all to end or all to start
639 public void rearrangeWaypoints(int inFunction)
641 UndoRearrangeWaypoints undo = new UndoRearrangeWaypoints(_track);
642 boolean success = false;
643 if (inFunction == REARRANGE_TO_START || inFunction == REARRANGE_TO_END)
645 // Collect the waypoints to the start or end of the track
646 success = _track.collectWaypoints(inFunction == REARRANGE_TO_START);
650 // Interleave the waypoints into track order
651 success = _track.interleaveWaypoints();
655 _undoStack.add(undo);
659 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.rearrange.noop"),
660 I18nManager.getText("error.function.noop.title"), JOptionPane.WARNING_MESSAGE);
666 * Open a new window with the 3d view
668 public void show3dWindow()
670 ThreeDWindow window = WindowFactory.getWindow(this, _frame);
673 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.function.nojava3d"),
674 I18nManager.getText("error.function.notavailable.title"), JOptionPane.WARNING_MESSAGE);
680 // Pass the track object and show the window
681 window.setTrack(_track);
684 catch (ThreeDException e)
686 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.3d") + ": " + e.getMessage(),
687 I18nManager.getText("error.3d.title"), JOptionPane.ERROR_MESSAGE);
696 public void selectAll()
698 _trackInfo.getSelection().select(0, 0, _track.getNumPoints()-1);
704 public void selectNone()
706 // deselect point, range and photo
707 _trackInfo.getSelection().clearAll();
712 * Receive loaded data and optionally merge with current Track
713 * @param inFieldArray array of fields
714 * @param inDataArray array of data
715 * @param inAltFormat altitude format
716 * @param inFilename filename used
718 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
720 // Check whether loaded array can be properly parsed into a Track
721 Track loadedTrack = new Track();
722 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
723 if (loadedTrack.getNumPoints() <= 0)
725 JOptionPane.showMessageDialog(_frame,
726 I18nManager.getText("error.load.nopoints"),
727 I18nManager.getText("error.load.dialogtitle"),
728 JOptionPane.ERROR_MESSAGE);
731 // Decide whether to load or append
732 if (_track != null && _track.getNumPoints() > 0)
734 // ask whether to replace or append
735 int answer = JOptionPane.showConfirmDialog(_frame,
736 I18nManager.getText("dialog.openappend.text"),
737 I18nManager.getText("dialog.openappend.title"),
738 JOptionPane.YES_NO_CANCEL_OPTION);
739 if (answer == JOptionPane.YES_OPTION)
741 // append data to current Track
742 _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
743 _track.combine(loadedTrack);
744 // set filename if currently empty
745 if (_trackInfo.getFileInfo().getNumFiles() == 0)
747 _trackInfo.getFileInfo().setFile(inFilename);
751 _trackInfo.getFileInfo().addFile();
754 else if (answer == JOptionPane.NO_OPTION)
756 // Don't append, replace data
757 PhotoList photos = null;
758 if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
760 photos = _trackInfo.getPhotoList().cloneList();
762 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
763 _lastSavePosition = _undoStack.size();
764 // TODO: Should be possible to reuse the Track object already loaded?
765 _trackInfo.selectPoint(null);
766 _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
767 _trackInfo.getFileInfo().setFile(inFilename);
770 _trackInfo.getPhotoList().removeCorrelatedPhotos();
776 // currently no data held, so use received data
777 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
778 _lastSavePosition = _undoStack.size();
779 _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
780 _trackInfo.getFileInfo().setFile(inFilename);
782 UpdateMessageBroker.informSubscribers();
784 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile") + " '" + inFilename + "'");
786 _menuManager.informFileLoaded();
791 * Accept a list of loaded photos
792 * @param inPhotoSet Set of Photo objects
794 public void informPhotosLoaded(Set inPhotoSet)
796 if (inPhotoSet != null && !inPhotoSet.isEmpty())
798 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
799 int numPhotosAdded = numsAdded[0];
800 int numPointsAdded = numsAdded[1];
801 if (numPhotosAdded > 0)
803 // Save numbers so load can be undone
804 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
806 if (numPhotosAdded == 1)
808 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
812 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
814 // TODO: Improve message when photo(s) fail to load (eg already added)
815 UpdateMessageBroker.informSubscribers();
817 _menuManager.informFileLoaded();
823 * Connect the current photo to the current point
825 public void connectPhotoToPoint()
827 Photo photo = _trackInfo.getCurrentPhoto();
828 DataPoint point = _trackInfo.getCurrentPoint();
829 if (photo != null && point != null && point.getPhoto() == null)
832 _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
833 photo.setDataPoint(point);
834 point.setPhoto(photo);
835 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
836 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
842 * Disconnect the current photo from its point
844 public void disconnectPhotoFromPoint()
846 Photo photo = _trackInfo.getCurrentPhoto();
847 if (photo != null && photo.getDataPoint() != null)
849 DataPoint point = photo.getDataPoint();
850 _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
852 photo.setDataPoint(null);
853 point.setPhoto(null);
854 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
855 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
861 * Remove the current photo, if any
863 public void deleteCurrentPhoto()
865 // Delete the current photo, and optionally its point too, keeping undo information
866 Photo currentPhoto = _trackInfo.getCurrentPhoto();
867 if (currentPhoto != null)
869 // Photo is selected, see if it has a point or not
870 boolean photoDeleted = false;
871 UndoDeletePhoto undoAction = null;
872 if (currentPhoto.getDataPoint() == null)
874 // no point attached, so just delete photo
875 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
877 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
881 // point is attached, so need to confirm point deletion
882 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
883 currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
884 int response = JOptionPane.showConfirmDialog(_frame,
885 I18nManager.getText("dialog.deletephoto.deletepoint"),
886 I18nManager.getText("dialog.deletephoto.title"),
887 JOptionPane.YES_NO_CANCEL_OPTION);
888 boolean deletePointToo = (response == JOptionPane.YES_OPTION);
889 // Cancel delete if cancel pressed or dialog closed
890 if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
892 photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
895 // Add undo information to stack if necessary
898 _undoStack.add(undoAction);
905 * Begin the photo correlation process by invoking dialog
907 public void beginCorrelatePhotos()
909 PhotoCorrelator correlator = new PhotoCorrelator(this, _frame);
910 // TODO: Do we need to keep a reference to this Photo Correlator object to reuse it later?
916 * Finish the photo correlation process
917 * @param inPointPairs array of PointPair objects describing operation
919 public void finishCorrelatePhotos(PointPair[] inPointPairs)
921 // TODO: This method is too big for App, but where should it go?
922 if (inPointPairs != null && inPointPairs.length > 0)
924 // begin to construct undo information
925 UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_trackInfo);
927 int arraySize = inPointPairs.length;
928 int i = 0, numPhotos = 0;
929 int numPointsToCreate = 0;
930 PointPair pair = null;
931 for (i=0; i<arraySize; i++)
933 pair = inPointPairs[i];
934 if (pair != null && pair.isValid())
936 if (pair.getMinSeconds() == 0L)
939 Photo pointPhoto = pair.getPointBefore().getPhoto();
940 if (pointPhoto == null)
942 // photo coincides with photoless point so connect the two
943 pair.getPointBefore().setPhoto(pair.getPhoto());
944 pair.getPhoto().setDataPoint(pair.getPointBefore());
946 else if (pointPhoto.equals(pair.getPhoto()))
948 // photo is already connected, nothing to do
952 // point is already connected to a different photo, so need to clone point
958 // photo time falls between two points, so need to interpolate new one
964 // Second loop, to create points if necessary
965 if (numPointsToCreate > 0)
967 // make new array for added points
968 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
970 DataPoint pointToAdd = null;
971 for (i=0; i<arraySize; i++)
973 pair = inPointPairs[i];
974 if (pair != null && pair.isValid())
977 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
978 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
981 pointToAdd = pair.getPointBefore().clonePoint();
983 else if (pair.getMinSeconds() > 0L)
986 pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
988 if (pointToAdd != null)
990 // link photo to point
991 pointToAdd.setPhoto(pair.getPhoto());
992 pair.getPhoto().setDataPoint(pointToAdd);
993 // add to point array
994 addedPoints[pointNum] = pointToAdd;
1000 _track.appendPoints(addedPoints);
1002 // add undo information to stack
1003 undo.setNumPhotosCorrelated(numPhotos);
1004 _undoStack.add(undo);
1005 // confirm correlation
1006 UpdateMessageBroker.informSubscribers("" + numPhotos + " "
1007 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi")));
1008 // observers already informed by track update
1014 * Save the coordinates of photos in their exif data
1016 public void saveExif()
1018 ExifSaver saver = new ExifSaver(_frame);
1019 saver.saveExifInformation(_trackInfo.getPhotoList());
1024 * Inform the app that the data has been saved
1026 public void informDataSaved()
1028 _lastSavePosition = _undoStack.size();
1033 * Begin undo process
1035 public void beginUndo()
1037 if (_undoStack.isEmpty())
1040 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
1041 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
1045 new UndoManager(this, _frame);
1051 * Clear the undo stack (losing all undo information
1053 public void clearUndo()
1055 // Exit if nothing to undo
1056 if (_undoStack == null || _undoStack.isEmpty())
1058 // Has track got unsaved data?
1059 boolean unsaved = hasDataUnsaved();
1060 // Confirm operation with dialog
1061 int answer = JOptionPane.showConfirmDialog(_frame,
1062 I18nManager.getText("dialog.clearundo.text"),
1063 I18nManager.getText("dialog.clearundo.title"),
1064 JOptionPane.YES_NO_OPTION);
1065 if (answer == JOptionPane.YES_OPTION)
1068 _lastSavePosition = 0;
1069 if (unsaved) _lastSavePosition = -1;
1070 UpdateMessageBroker.informSubscribers();
1076 * Undo the specified number of actions
1077 * @param inNumUndos number of actions to undo
1079 public void undoActions(int inNumUndos)
1083 for (int i=0; i<inNumUndos; i++)
1085 ((UndoOperation) _undoStack.pop()).performUndo(_trackInfo);
1087 String message = "" + inNumUndos + " "
1088 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
1089 UpdateMessageBroker.informSubscribers(message);
1091 catch (UndoException ue)
1093 JOptionPane.showMessageDialog(_frame,
1094 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage(),
1095 I18nManager.getText("error.undofailed.title"),
1096 JOptionPane.ERROR_MESSAGE);
1098 UpdateMessageBroker.informSubscribers();
1100 catch (EmptyStackException empty) {}
1105 * Helper method to parse an Object into an integer
1106 * @param inObject object, eg from dialog
1107 * @return int value given
1109 private static int parseNumber(Object inObject)
1112 if (inObject != null)
1116 num = Integer.parseInt(inObject.toString());
1118 catch (NumberFormatException nfe)
1125 * Show a brief help message
1127 public void showHelp()
1129 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.help.help"),
1130 I18nManager.getText("menu.help"),
1131 JOptionPane.INFORMATION_MESSAGE);
1135 * Show an OSM map window
1137 public void showOsmMap()
1139 MapWindow map = new MapWindow(_track);
1145 * Show a map url in an external browser
1147 public void showExternalMap(int inSourceIndex)
1149 if (_browserLauncher == null) {_browserLauncher = new BrowserLauncher();}
1150 _browserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));