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.Coordinate;
15 import tim.prune.data.DataPoint;
16 import tim.prune.data.Field;
17 import tim.prune.data.LatLonRectangle;
18 import tim.prune.data.Latitude;
19 import tim.prune.data.Longitude;
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.edit.FieldEditList;
25 import tim.prune.edit.PointEditor;
26 import tim.prune.edit.PointNameEditor;
27 import tim.prune.gui.MenuManager;
28 import tim.prune.gui.TimeOffsetDialog;
29 import tim.prune.gui.UndoManager;
30 import tim.prune.load.FileLoader;
31 import tim.prune.load.GpsLoader;
32 import tim.prune.load.JpegLoader;
33 import tim.prune.save.ExifSaver;
34 import tim.prune.save.FileSaver;
35 import tim.prune.save.GpxExporter;
36 import tim.prune.save.KmlExporter;
37 import tim.prune.save.PovExporter;
38 import tim.prune.threedee.ThreeDException;
39 import tim.prune.threedee.ThreeDWindow;
40 import tim.prune.threedee.WindowFactory;
41 import tim.prune.undo.UndoAddTimeOffset;
42 import tim.prune.undo.UndoCompress;
43 import tim.prune.undo.UndoConnectPhoto;
44 import tim.prune.undo.UndoConnectPhotoWithClone;
45 import tim.prune.undo.UndoCorrelatePhotos;
46 import tim.prune.undo.UndoCreatePoint;
47 import tim.prune.undo.UndoCutAndMove;
48 import tim.prune.undo.UndoDeleteDuplicates;
49 import tim.prune.undo.UndoDeletePhoto;
50 import tim.prune.undo.UndoDeletePoint;
51 import tim.prune.undo.UndoDeleteRange;
52 import tim.prune.undo.UndoDisconnectPhoto;
53 import tim.prune.undo.UndoEditPoint;
54 import tim.prune.undo.UndoException;
55 import tim.prune.undo.UndoInsert;
56 import tim.prune.undo.UndoLoad;
57 import tim.prune.undo.UndoLoadPhotos;
58 import tim.prune.undo.UndoMergeTrackSegments;
59 import tim.prune.undo.UndoOperation;
60 import tim.prune.undo.UndoRearrangeWaypoints;
61 import tim.prune.undo.UndoReverseSection;
65 * Main controller for the application
70 private JFrame _frame = null;
71 private Track _track = null;
72 private TrackInfo _trackInfo = null;
73 private int _lastSavePosition = 0;
74 private MenuManager _menuManager = null;
75 private FileLoader _fileLoader = null;
76 private JpegLoader _jpegLoader = null;
77 private GpsLoader _gpsLoader = null;
78 private FileSaver _fileSaver = null;
79 private KmlExporter _kmlExporter = null;
80 private GpxExporter _gpxExporter = null;
81 private PovExporter _povExporter = null;
82 private BrowserLauncher _browserLauncher = null;
83 private Stack _undoStack = null;
84 private boolean _mangleTimestampsConfirmed = false;
87 public static final int REARRANGE_TO_START = 0;
88 public static final int REARRANGE_TO_END = 1;
89 public static final int REARRANGE_TO_NEAREST = 2;
94 * @param inFrame frame object for application
96 public App(JFrame inFrame)
99 _undoStack = new Stack();
100 _track = new Track();
101 _trackInfo = new TrackInfo(_track);
106 * @return the current TrackInfo
108 public TrackInfo getTrackInfo()
114 * Check if the application has unsaved data
115 * @return true if data is unsaved, false otherwise
117 public boolean hasDataUnsaved()
119 return (_undoStack.size() > _lastSavePosition
120 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
124 * @return the undo stack
126 public Stack getUndoStack()
132 * Set the MenuManager object to be informed about changes
133 * @param inManager MenuManager object
135 public void setMenuManager(MenuManager inManager)
137 _menuManager = inManager;
142 * Open a file containing track or waypoint data
144 public void openFile()
146 if (_fileLoader == null)
147 _fileLoader = new FileLoader(this, _frame);
148 _fileLoader.openFile();
153 * Add a photo or a directory of photos
155 public void addPhotos()
157 if (_jpegLoader == null)
158 _jpegLoader = new JpegLoader(this, _frame);
159 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
163 * Start a load from Gps
165 public void beginLoadFromGps()
167 if (_gpsLoader == null)
168 _gpsLoader = new GpsLoader(this, _frame);
169 _gpsLoader.openDialog();
173 * Save the file in the selected format
175 public void saveFile()
179 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
180 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
184 if (_fileSaver == null) {
185 _fileSaver = new FileSaver(this, _frame, _track);
188 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
189 _fileSaver.showDialog(delim);
195 * Export track data as Kml
197 public void exportKml()
201 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
202 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
207 if (_kmlExporter == null)
209 _kmlExporter = new KmlExporter(_frame, _trackInfo);
211 _kmlExporter.showDialog();
217 * Export track data as Gpx
219 public void exportGpx()
223 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
224 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
229 if (_gpxExporter == null)
231 _gpxExporter = new GpxExporter(_frame, _trackInfo);
233 _gpxExporter.showDialog();
239 * Export track data as Pov without specifying settings
241 public void exportPov()
243 exportPov(false, 0.0, 0.0, 0.0, 0);
247 * Export track data as Pov and also specify settings
248 * @param inX X component of unit vector
249 * @param inY Y component of unit vector
250 * @param inZ Z component of unit vector
251 * @param inAltitudeCap altitude cap
253 public void exportPov(double inX, double inY, double inZ, int inAltitudeCap)
255 exportPov(true, inX, inY, inZ, inAltitudeCap);
259 * Export track data as Pov with optional angle specification
260 * @param inDefineAngles true to define angles, false to ignore
261 * @param inX X component of unit vector
262 * @param inY Y component of unit vector
263 * @param inZ Z component of unit vector
264 * @param inAltitudeCap altitude cap
266 private void exportPov(boolean inDefineSettings, double inX, double inY, double inZ, int inAltitudeCap)
268 // Check track has data to export
269 if (_track == null || _track.getNumPoints() <= 0)
271 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
272 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
276 // Make new exporter if necessary
277 if (_povExporter == null)
279 _povExporter = new PovExporter(_frame, _track);
281 // Specify angles if necessary
282 if (inDefineSettings)
284 _povExporter.setCameraCoordinates(inX, inY, inZ);
285 _povExporter.setAltitudeCap(inAltitudeCap);
288 _povExporter.showDialog();
294 * Exit the application if confirmed
300 _frame.requestFocus();
301 // check if ok to exit
302 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
303 if (!hasDataUnsaved()
304 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
305 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
306 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
307 == JOptionPane.YES_OPTION)
315 * Edit the currently selected point
317 public void editCurrentPoint()
321 DataPoint currentPoint = _trackInfo.getCurrentPoint();
322 if (currentPoint != null)
324 // Open point dialog to display details
325 PointEditor editor = new PointEditor(this, _frame);
326 editor.showDialog(_track, currentPoint);
333 * Complete the point edit
334 * @param inEditList field values to edit
335 * @param inUndoList field values before edit
337 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
339 DataPoint currentPoint = _trackInfo.getCurrentPoint();
340 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
342 // add information to undo stack
343 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
344 // pass to track for completion
345 if (_track.editPoint(currentPoint, inEditList))
347 _undoStack.push(undo);
348 // Confirm point edit
349 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
356 * Edit the name of the currently selected (way)point
358 public void editCurrentPointName()
362 DataPoint currentPoint = _trackInfo.getCurrentPoint();
363 if (currentPoint != null)
365 // Open point dialog to display details
366 PointNameEditor editor = new PointNameEditor(this, _frame);
367 editor.showDialog(currentPoint);
374 * Delete the currently selected point
376 public void deleteCurrentPoint()
378 if (_track == null) {return;}
379 DataPoint currentPoint = _trackInfo.getCurrentPoint();
380 if (currentPoint != null)
382 boolean deletePhoto = false;
383 Photo currentPhoto = currentPoint.getPhoto();
384 if (currentPhoto != null)
386 // Confirm deletion of photo or decoupling
387 int response = JOptionPane.showConfirmDialog(_frame,
388 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
389 I18nManager.getText("dialog.deletepoint.title"),
390 JOptionPane.YES_NO_CANCEL_OPTION);
391 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
393 // cancel pressed- abort delete
396 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
398 // store necessary information to undo it later
399 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
400 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
401 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
402 // Construct Undo object
403 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
404 nextTrackPoint != null && nextTrackPoint.getSegmentStart());
405 // call track to delete point
406 if (_trackInfo.deletePoint())
408 // Delete was successful so add undo info to stack
409 _undoStack.push(undo);
410 if (currentPhoto != null)
412 // delete photo if necessary
415 _trackInfo.getPhotoList().deletePhoto(photoIndex);
419 // decouple photo from point
420 currentPhoto.setDataPoint(null);
424 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
431 * Delete the currently selected range
433 public void deleteSelectedRange()
437 // Find out if photos should be deleted or not
438 int selStart = _trackInfo.getSelection().getStart();
439 int selEnd = _trackInfo.getSelection().getEnd();
440 if (selStart >= 0 && selEnd >= selStart)
442 int numToDelete = selEnd - selStart + 1;
443 boolean[] deletePhotos = new boolean[numToDelete];
444 Photo[] photosToDelete = new Photo[numToDelete];
445 boolean deleteAll = false;
446 boolean deleteNone = false;
447 String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
448 I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
449 I18nManager.getText("button.cancel")};
450 DataPoint point = null;
451 for (int i=0; i<numToDelete; i++)
453 point = _trackInfo.getTrack().getPoint(i + selStart);
454 if (point != null && point.getPhoto() != null)
458 deletePhotos[i] = true;
459 photosToDelete[i] = point.getPhoto();
461 else if (deleteNone) {deletePhotos[i] = false;}
464 int response = JOptionPane.showOptionDialog(_frame,
465 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
466 I18nManager.getText("dialog.deletepoint.title"),
467 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
468 questionOptions, questionOptions[1]);
469 // check for cancel or close
470 if (response == 4 || response == -1) {return;}
471 // check for yes or yes to all
472 if (response == 0 || response == 2)
474 deletePhotos[i] = true;
475 photosToDelete[i] = point.getPhoto();
476 if (response == 2) {deleteAll = true;}
478 // check for no to all
479 if (response == 3) {deleteNone = true;}
483 // add information to undo stack
484 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
485 // delete requested photos
486 for (int i=0; i<numToDelete; i++)
488 point = _trackInfo.getTrack().getPoint(i + selStart);
489 if (point != null && point.getPhoto() != null)
493 // delete photo from list
494 _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
498 // decouple from point
499 point.getPhoto().setDataPoint(null);
503 // call track to delete range
504 if (_trackInfo.deleteRange())
506 _undoStack.push(undo);
508 UpdateMessageBroker.informSubscribers("" + numToDelete + " "
509 + I18nManager.getText("confirm.deletepoint.multi"));
517 * Delete all the duplicate points in the track
519 public void deleteDuplicates()
523 // Save undo information
524 UndoOperation undo = new UndoDeleteDuplicates(_track);
525 // tell track to do it
526 int numDeleted = _trackInfo.deleteDuplicates();
529 _undoStack.add(undo);
530 String message = null;
533 message = "1 " + I18nManager.getText("confirm.deleteduplicates.single");
537 message = "" + numDeleted + " " + I18nManager.getText("confirm.deleteduplicates.multi");
539 // Pass message to broker
540 UpdateMessageBroker.informSubscribers(message);
544 // No duplicates found to delete
545 JOptionPane.showMessageDialog(_frame,
546 I18nManager.getText("dialog.deleteduplicates.nonefound"),
547 I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE);
556 public void compressTrack()
558 UndoCompress undo = new UndoCompress(_track);
559 // Get compression parameter
560 Object compParam = JOptionPane.showInputDialog(_frame,
561 I18nManager.getText("dialog.compresstrack.parameter.text"),
562 I18nManager.getText("dialog.compresstrack.title"),
563 JOptionPane.QUESTION_MESSAGE, null, null, "100");
564 int compNumber = parseNumber(compParam);
565 if (compNumber <= 0) return;
566 // call track to do compress
567 int numPointsDeleted = _trackInfo.compress(compNumber);
568 // add to undo stack if successful
569 if (numPointsDeleted > 0)
571 undo.setNumPointsDeleted(numPointsDeleted);
572 _undoStack.add(undo);
573 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
574 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
578 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.compresstrack.nonefound"),
579 I18nManager.getText("dialog.compresstrack.title"), JOptionPane.WARNING_MESSAGE);
585 * Reverse the currently selected section of the track
587 public void reverseRange()
589 // check whether Timestamp field exists, and if so confirm reversal
590 int selStart = _trackInfo.getSelection().getStart();
591 int selEnd = _trackInfo.getSelection().getEnd();
592 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
593 || _mangleTimestampsConfirmed
594 || (JOptionPane.showConfirmDialog(_frame,
595 I18nManager.getText("dialog.confirmreversetrack.text"),
596 I18nManager.getText("dialog.confirmreversetrack.title"),
597 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
599 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
600 // call track to reverse range
601 if (_track.reverseRange(selStart, selEnd))
603 _undoStack.add(undo);
605 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
611 * Trigger the dialog to add a time offset to the current selection
613 public void beginAddTimeOffset()
615 int selStart = _trackInfo.getSelection().getStart();
616 int selEnd = _trackInfo.getSelection().getEnd();
617 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)) {
618 JOptionPane.showMessageDialog(_frame,
619 I18nManager.getText("dialog.addtimeoffset.notimestamps"),
620 I18nManager.getText("dialog.addtimeoffset.title"), JOptionPane.ERROR_MESSAGE);
623 TimeOffsetDialog timeDialog = new TimeOffsetDialog(this, _frame);
624 timeDialog.showDialog();
629 * Complete the add time offset function with the specified offset
630 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
632 public void finishAddTimeOffset(long inTimeOffset)
634 // Construct undo information
635 int selStart = _trackInfo.getSelection().getStart();
636 int selEnd = _trackInfo.getSelection().getEnd();
637 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
638 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset))
640 _undoStack.add(undo);
641 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
642 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
648 * Merge the track segments within the current selection
650 public void mergeTrackSegments()
652 if (_trackInfo.getSelection().hasRangeSelected())
654 // Maybe could check segment start flags to see if it's worth merging
655 // If first track point is already start and no other seg starts then do nothing
657 int selStart = _trackInfo.getSelection().getStart();
658 int selEnd = _trackInfo.getSelection().getEnd();
660 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
661 // Call track to merge segments
662 if (_track.mergeTrackSegments(selStart, selEnd)) {
663 _undoStack.add(undo);
664 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
671 * Interpolate the two selected points
673 public void interpolateSelection()
675 // Get number of points to add
676 Object numPointsStr = JOptionPane.showInputDialog(_frame,
677 I18nManager.getText("dialog.interpolate.parameter.text"),
678 I18nManager.getText("dialog.interpolate.title"),
679 JOptionPane.QUESTION_MESSAGE, null, null, "");
680 int numPoints = parseNumber(numPointsStr);
681 if (numPoints <= 0) return;
683 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
685 // call track to interpolate
686 if (_trackInfo.interpolate(numPoints))
688 _undoStack.add(undo);
694 * Create a new point at the given lat/long coordinates
695 * @param inLat latitude
696 * @param inLong longitude
698 public void createPoint(double inLat, double inLong)
700 // create undo object
701 UndoCreatePoint undo = new UndoCreatePoint();
702 // create point and add to track
703 DataPoint point = new DataPoint(new Latitude(inLat, Coordinate.FORMAT_NONE), new Longitude(inLong, Coordinate.FORMAT_NONE), null);
704 point.setSegmentStart(true);
705 _track.appendPoints(new DataPoint[] {point});
706 _trackInfo.getSelection().selectPoint(_trackInfo.getTrack().getNumPoints()-1);
707 // add undo object to stack
708 _undoStack.add(undo);
710 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
715 * Rearrange the waypoints into track order
716 * @param inFunction nearest point, all to end or all to start
718 public void rearrangeWaypoints(int inFunction)
720 UndoRearrangeWaypoints undo = new UndoRearrangeWaypoints(_track);
721 boolean success = false;
722 if (inFunction == REARRANGE_TO_START || inFunction == REARRANGE_TO_END)
724 // Collect the waypoints to the start or end of the track
725 success = _track.collectWaypoints(inFunction == REARRANGE_TO_START);
729 // Interleave the waypoints into track order
730 success = _track.interleaveWaypoints();
734 _undoStack.add(undo);
735 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.rearrangewaypoints"));
739 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.rearrange.noop"),
740 I18nManager.getText("error.function.noop.title"), JOptionPane.WARNING_MESSAGE);
746 * Cut the current selection and move it to before the currently selected point
748 public void cutAndMoveSelection()
750 int startIndex = _trackInfo.getSelection().getStart();
751 int endIndex = _trackInfo.getSelection().getEnd();
752 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
753 // If timestamps would be mangled by cut/move, confirm
754 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
755 || _mangleTimestampsConfirmed
756 || (JOptionPane.showConfirmDialog(_frame,
757 I18nManager.getText("dialog.confirmcutandmove.text"),
758 I18nManager.getText("dialog.confirmcutandmove.title"),
759 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
761 // Find points to set segment flags
762 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
763 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
764 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
766 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
767 // Call track info to move track section
768 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
770 // Set segment start flags (first track point, next track point, move to point)
771 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
772 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
773 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
775 // Add undo object to stack, set confirm message
776 _undoStack.add(undo);
777 _trackInfo.getSelection().deselectRange();
778 UpdateMessageBroker.informSubscribers();
779 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
786 * Open a new window with the 3d view
788 public void show3dWindow()
790 ThreeDWindow window = WindowFactory.getWindow(this, _frame);
793 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.function.nojava3d"),
794 I18nManager.getText("error.function.notavailable.title"), JOptionPane.WARNING_MESSAGE);
800 // Pass the track object and show the window
801 window.setTrack(_track);
804 catch (ThreeDException e)
806 JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.3d") + ": " + e.getMessage(),
807 I18nManager.getText("error.3d.title"), JOptionPane.ERROR_MESSAGE);
816 public void selectAll()
818 _trackInfo.getSelection().select(0, 0, _track.getNumPoints()-1);
824 public void selectNone()
826 // deselect point, range and photo
827 _trackInfo.getSelection().clearAll();
832 * Receive loaded data and optionally merge with current Track
833 * @param inFieldArray array of fields
834 * @param inDataArray array of data
835 * @param inAltFormat altitude format
836 * @param inFilename filename used
838 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
840 informDataLoaded(inFieldArray, inDataArray, inAltFormat, inFilename, false);
844 * Receive loaded data and optionally merge with current Track
845 * @param inFieldArray array of fields
846 * @param inDataArray array of data
847 * @param inAltFormat altitude format
848 * @param inFilename filename used
849 * @param inOverrideAppend true to override append question and always append
851 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat,
852 String inFilename, boolean inOverrideAppend)
854 // Check whether loaded array can be properly parsed into a Track
855 Track loadedTrack = new Track();
856 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
857 if (loadedTrack.getNumPoints() <= 0)
859 JOptionPane.showMessageDialog(_frame,
860 I18nManager.getText("error.load.nopoints"),
861 I18nManager.getText("error.load.dialogtitle"),
862 JOptionPane.ERROR_MESSAGE);
865 // Decide whether to load or append
866 if (_track != null && _track.getNumPoints() > 0)
868 // ask whether to replace or append
869 int answer = JOptionPane.YES_OPTION;
870 if (!inOverrideAppend) {
871 answer = JOptionPane.showConfirmDialog(_frame,
872 I18nManager.getText("dialog.openappend.text"),
873 I18nManager.getText("dialog.openappend.title"),
874 JOptionPane.YES_NO_CANCEL_OPTION);
876 if (answer == JOptionPane.YES_OPTION)
878 // append data to current Track
879 _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints()));
880 _track.combine(loadedTrack);
881 // set filename if currently empty
882 if (_trackInfo.getFileInfo().getNumFiles() == 0)
884 _trackInfo.getFileInfo().setFile(inFilename);
888 _trackInfo.getFileInfo().addFile();
891 else if (answer == JOptionPane.NO_OPTION)
893 // Don't append, replace data
894 PhotoList photos = null;
895 if (_trackInfo.getPhotoList().hasCorrelatedPhotos())
897 photos = _trackInfo.getPhotoList().cloneList();
899 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
900 _lastSavePosition = _undoStack.size();
901 // TODO: Should be possible to reuse the Track object already loaded?
902 _trackInfo.selectPoint(null);
903 _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
904 _trackInfo.getFileInfo().setFile(inFilename);
907 _trackInfo.getPhotoList().removeCorrelatedPhotos();
913 // currently no data held, so use received data
914 _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null));
915 _lastSavePosition = _undoStack.size();
916 _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
917 _trackInfo.getFileInfo().setFile(inFilename);
919 UpdateMessageBroker.informSubscribers();
921 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile") + " '" + inFilename + "'");
923 _menuManager.informFileLoaded();
928 * Accept a list of loaded photos
929 * @param inPhotoSet Set of Photo objects
931 public void informPhotosLoaded(Set inPhotoSet)
933 if (inPhotoSet != null && !inPhotoSet.isEmpty())
935 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
936 int numPhotosAdded = numsAdded[0];
937 int numPointsAdded = numsAdded[1];
938 if (numPhotosAdded > 0)
940 // Save numbers so load can be undone
941 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
943 if (numPhotosAdded == 1)
945 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
949 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
951 // TODO: Improve message when photo(s) fail to load (eg already added)
952 UpdateMessageBroker.informSubscribers();
954 _menuManager.informFileLoaded();
960 * Connect the current photo to the current point
962 public void connectPhotoToPoint()
964 Photo photo = _trackInfo.getCurrentPhoto();
965 DataPoint point = _trackInfo.getCurrentPoint();
966 if (photo != null && point != null)
968 if (point.getPhoto() != null)
970 // point already has a photo, confirm cloning of new point
971 if (JOptionPane.showConfirmDialog(_frame,
972 I18nManager.getText("dialog.connectphoto.clonepoint"),
973 I18nManager.getText("dialog.connect.title"),
974 JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION)
976 // Create undo, clone point and attach
977 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex() + 1;
978 // insert new point after current one
979 point = point.clonePoint();
980 UndoConnectPhotoWithClone undo = new UndoConnectPhotoWithClone(
981 point, photo.getFile().getName(), pointIndex);
982 _track.insertPoint(point, pointIndex);
983 photo.setDataPoint(point);
984 point.setPhoto(photo);
985 _undoStack.add(undo);
986 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
987 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
992 // point doesn't currently have a photo, so just connect it
993 _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
994 photo.setDataPoint(point);
995 point.setPhoto(photo);
996 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
997 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
1004 * Disconnect the current photo from its point
1006 public void disconnectPhotoFromPoint()
1008 Photo photo = _trackInfo.getCurrentPhoto();
1009 if (photo != null && photo.getDataPoint() != null)
1011 DataPoint point = photo.getDataPoint();
1012 _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
1014 photo.setDataPoint(null);
1015 point.setPhoto(null);
1016 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
1017 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
1023 * Remove the current photo, if any
1025 public void deleteCurrentPhoto()
1027 // Delete the current photo, and optionally its point too, keeping undo information
1028 Photo currentPhoto = _trackInfo.getCurrentPhoto();
1029 if (currentPhoto != null)
1031 // Photo is selected, see if it has a point or not
1032 boolean photoDeleted = false;
1033 UndoDeletePhoto undoAction = null;
1034 if (currentPhoto.getDataPoint() == null)
1036 // no point attached, so just delete photo
1037 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
1039 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
1043 // point is attached, so need to confirm point deletion
1044 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
1045 currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
1046 int response = JOptionPane.showConfirmDialog(_frame,
1047 I18nManager.getText("dialog.deletephoto.deletepoint"),
1048 I18nManager.getText("dialog.deletephoto.title"),
1049 JOptionPane.YES_NO_CANCEL_OPTION);
1050 boolean deletePointToo = (response == JOptionPane.YES_OPTION);
1051 // Cancel delete if cancel pressed or dialog closed
1052 if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
1054 photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
1057 // Add undo information to stack if necessary
1060 _undoStack.add(undoAction);
1067 * Begin the photo correlation process by invoking dialog
1069 public void beginCorrelatePhotos()
1071 PhotoCorrelator correlator = new PhotoCorrelator(this, _frame);
1072 // TODO: Do we need to keep a reference to this Photo Correlator object to reuse it later?
1078 * Finish the photo correlation process
1079 * @param inPointPairs array of PointPair objects describing operation
1081 public void finishCorrelatePhotos(PointPair[] inPointPairs)
1083 // TODO: This method is too big for App, but where should it go?
1084 if (inPointPairs != null && inPointPairs.length > 0)
1086 // begin to construct undo information
1087 UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_trackInfo);
1089 int arraySize = inPointPairs.length;
1090 int i = 0, numPhotos = 0;
1091 int numPointsToCreate = 0;
1092 PointPair pair = null;
1093 for (i=0; i<arraySize; i++)
1095 pair = inPointPairs[i];
1096 if (pair != null && pair.isValid())
1098 if (pair.getMinSeconds() == 0L)
1101 Photo pointPhoto = pair.getPointBefore().getPhoto();
1102 if (pointPhoto == null)
1104 // photo coincides with photoless point so connect the two
1105 pair.getPointBefore().setPhoto(pair.getPhoto());
1106 pair.getPhoto().setDataPoint(pair.getPointBefore());
1108 else if (pointPhoto.equals(pair.getPhoto()))
1110 // photo is already connected, nothing to do
1114 // point is already connected to a different photo, so need to clone point
1115 numPointsToCreate++;
1120 // photo time falls between two points, so need to interpolate new one
1121 numPointsToCreate++;
1126 // Second loop, to create points if necessary
1127 if (numPointsToCreate > 0)
1129 // make new array for added points
1130 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
1132 DataPoint pointToAdd = null;
1133 for (i=0; i<arraySize; i++)
1135 pair = inPointPairs[i];
1136 if (pair != null && pair.isValid())
1139 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
1140 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
1143 pointToAdd = pair.getPointBefore().clonePoint();
1145 else if (pair.getMinSeconds() > 0L)
1147 // interpolate point
1148 pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
1150 if (pointToAdd != null)
1152 // link photo to point
1153 pointToAdd.setPhoto(pair.getPhoto());
1154 pair.getPhoto().setDataPoint(pointToAdd);
1155 // set to start of segment so not joined in track
1156 pointToAdd.setSegmentStart(true);
1157 // add to point array
1158 addedPoints[pointNum] = pointToAdd;
1164 _track.appendPoints(addedPoints);
1166 // add undo information to stack
1167 undo.setNumPhotosCorrelated(numPhotos);
1168 _undoStack.add(undo);
1169 // confirm correlation
1170 UpdateMessageBroker.informSubscribers("" + numPhotos + " "
1171 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi")));
1172 // observers already informed by track update
1178 * Save the coordinates of photos in their exif data
1180 public void saveExif()
1182 ExifSaver saver = new ExifSaver(_frame);
1183 saver.saveExifInformation(_trackInfo.getPhotoList());
1188 * Inform the app that the data has been saved
1190 public void informDataSaved()
1192 _lastSavePosition = _undoStack.size();
1197 * Begin undo process
1199 public void beginUndo()
1201 if (_undoStack.isEmpty())
1204 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
1205 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
1209 new UndoManager(this, _frame);
1215 * Clear the undo stack (losing all undo information
1217 public void clearUndo()
1219 // Exit if nothing to undo
1220 if (_undoStack == null || _undoStack.isEmpty())
1222 // Has track got unsaved data?
1223 boolean unsaved = hasDataUnsaved();
1224 // Confirm operation with dialog
1225 int answer = JOptionPane.showConfirmDialog(_frame,
1226 I18nManager.getText("dialog.clearundo.text"),
1227 I18nManager.getText("dialog.clearundo.title"),
1228 JOptionPane.YES_NO_OPTION);
1229 if (answer == JOptionPane.YES_OPTION)
1232 _lastSavePosition = 0;
1233 if (unsaved) _lastSavePosition = -1;
1234 UpdateMessageBroker.informSubscribers();
1240 * Undo the specified number of actions
1241 * @param inNumUndos number of actions to undo
1243 public void undoActions(int inNumUndos)
1247 for (int i=0; i<inNumUndos; i++)
1249 ((UndoOperation) _undoStack.pop()).performUndo(_trackInfo);
1251 String message = "" + inNumUndos + " "
1252 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
1253 UpdateMessageBroker.informSubscribers(message);
1255 catch (UndoException ue)
1257 JOptionPane.showMessageDialog(_frame,
1258 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage(),
1259 I18nManager.getText("error.undofailed.title"),
1260 JOptionPane.ERROR_MESSAGE);
1262 UpdateMessageBroker.informSubscribers();
1264 catch (EmptyStackException empty) {}
1269 * Helper method to parse an Object into an integer
1270 * @param inObject object, eg from dialog
1271 * @return int value given
1273 private static int parseNumber(Object inObject)
1276 if (inObject != null)
1280 num = Integer.parseInt(inObject.toString());
1282 catch (NumberFormatException nfe)
1289 * Show a brief help message
1291 public void showHelp()
1293 // show the dialog and offer to open home page
1294 Object[] buttonTexts = {I18nManager.getText("button.showwebpage"), I18nManager.getText("button.cancel")};
1295 if (JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.help.help"),
1296 I18nManager.getText("menu.help"), JOptionPane.YES_NO_OPTION,
1297 JOptionPane.INFORMATION_MESSAGE, null, buttonTexts, buttonTexts[1])
1298 == JOptionPane.YES_OPTION)
1300 // User selected to launch home page
1301 if (_browserLauncher == null) {_browserLauncher = new BrowserLauncher();}
1302 _browserLauncher.launchBrowser("http://activityworkshop.net/software/prune/index.html");
1307 * Show a map url in an external browser
1309 public void showExternalMap(int inSourceIndex)
1311 if (_browserLauncher == null) {_browserLauncher = new BrowserLauncher();}
1312 _browserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));