4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
7 import java.util.Stack;
9 import javax.swing.JFrame;
10 import javax.swing.JOptionPane;
12 import tim.prune.data.Altitude;
13 import tim.prune.data.Checker;
14 import tim.prune.data.DataPoint;
15 import tim.prune.data.Field;
16 import tim.prune.data.LatLonRectangle;
17 import tim.prune.data.NumberUtils;
18 import tim.prune.data.Photo;
19 import tim.prune.data.PhotoList;
20 import tim.prune.data.SourceInfo;
21 import tim.prune.data.Track;
22 import tim.prune.data.TrackInfo;
23 import tim.prune.function.SelectTracksFunction;
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.SidebarController;
29 import tim.prune.gui.MenuManager;
30 import tim.prune.gui.UndoManager;
31 import tim.prune.gui.Viewport;
32 import tim.prune.load.FileLoader;
33 import tim.prune.load.JpegLoader;
34 import tim.prune.load.TrackNameList;
35 import tim.prune.save.ExifSaver;
36 import tim.prune.save.FileSaver;
37 import tim.prune.undo.*;
41 * Main controller for the application
46 private JFrame _frame = null;
47 private Track _track = null;
48 private TrackInfo _trackInfo = null;
49 private int _lastSavePosition = 0;
50 private MenuManager _menuManager = null;
51 private SidebarController __sidebarController = null;
52 private FileLoader _fileLoader = null;
53 private JpegLoader _jpegLoader = null;
54 private FileSaver _fileSaver = null;
55 private Stack<UndoOperation> _undoStack = null;
56 private boolean _mangleTimestampsConfirmed = false;
57 private Viewport _viewport = null;
58 private ArrayList<File> _dataFiles = null;
59 private boolean _firstDataFile = true;
64 * @param inFrame frame object for application
66 public App(JFrame inFrame)
69 _undoStack = new Stack<UndoOperation>();
71 _trackInfo = new TrackInfo(_track);
72 FunctionLibrary.initialise(this);
77 * @return the current TrackInfo
79 public TrackInfo getTrackInfo()
85 * @return the dialog frame
87 public JFrame getFrame()
93 * Check if the application has unsaved data
94 * @return true if data is unsaved, false otherwise
96 public boolean hasDataUnsaved()
98 return (_undoStack.size() > _lastSavePosition
99 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0));
103 * @return the undo stack
105 public Stack<UndoOperation> getUndoStack()
111 * Load the specified data files one by one
112 * @param inDataFiles arraylist containing File objects to load
114 public void loadDataFiles(ArrayList<File> inDataFiles)
116 if (inDataFiles == null || inDataFiles.size() == 0) {
120 _dataFiles = inDataFiles;
121 File f = _dataFiles.get(0);
122 _dataFiles.remove(0);
123 // Start load of specified file
124 if (_fileLoader == null)
125 _fileLoader = new FileLoader(this, _frame);
126 _firstDataFile = true;
127 _fileLoader.openFile(f);
132 * Complete a function execution
133 * @param inUndo undo object to be added to stack
134 * @param inConfirmText confirmation text
136 public void completeFunction(UndoOperation inUndo, String inConfirmText)
138 _undoStack.add(inUndo);
139 UpdateMessageBroker.informSubscribers(inConfirmText);
143 * Set the MenuManager object to be informed about changes
144 * @param inManager MenuManager object
146 public void setMenuManager(MenuManager inManager)
148 _menuManager = inManager;
153 * Open a file containing track or waypoint data
155 public void openFile()
157 if (_fileLoader == null)
158 _fileLoader = new FileLoader(this, _frame);
159 _fileLoader.openFile();
164 * Add a photo or a directory of photos
166 public void addPhotos()
168 if (_jpegLoader == null)
169 _jpegLoader = new JpegLoader(this, _frame);
170 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
174 * Save the file in the selected format
176 public void saveFile()
178 if (_track == null) {
179 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
183 if (_fileSaver == null) {
184 _fileSaver = new FileSaver(this, _frame);
187 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
188 _fileSaver.showDialog(delim);
194 * Exit the application if confirmed
200 _frame.requestFocus();
201 // check if ok to exit
202 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
203 if (!hasDataUnsaved()
204 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
205 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
206 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
207 == JOptionPane.YES_OPTION)
215 * Edit the currently selected point
217 public void editCurrentPoint()
221 DataPoint currentPoint = _trackInfo.getCurrentPoint();
222 if (currentPoint != null)
224 // Open point dialog to display details
225 PointEditor editor = new PointEditor(this, _frame);
226 editor.showDialog(_track, currentPoint);
233 * Complete the point edit
234 * @param inEditList field values to edit
235 * @param inUndoList field values before edit
237 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
239 DataPoint currentPoint = _trackInfo.getCurrentPoint();
240 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
242 // add information to undo stack
243 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
244 // pass to track for completion
245 if (_track.editPoint(currentPoint, inEditList, false))
247 _undoStack.push(undo);
248 // Confirm point edit
249 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
256 * Delete the currently selected point
258 public void deleteCurrentPoint()
260 if (_track == null) {return;}
261 DataPoint currentPoint = _trackInfo.getCurrentPoint();
262 if (currentPoint != null)
264 boolean deletePhoto = false;
265 Photo currentPhoto = currentPoint.getPhoto();
266 if (currentPhoto != null)
268 // Confirm deletion of photo or decoupling
269 int response = JOptionPane.showConfirmDialog(_frame,
270 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(),
271 I18nManager.getText("dialog.deletepoint.title"),
272 JOptionPane.YES_NO_CANCEL_OPTION);
273 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
275 // cancel pressed- abort delete
278 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
280 // store necessary information to undo it later
281 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
282 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
283 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
284 // Construct Undo object
285 UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
286 nextTrackPoint != null && nextTrackPoint.getSegmentStart());
287 // call track to delete point
288 if (_trackInfo.deletePoint())
290 // Delete was successful so add undo info to stack
291 _undoStack.push(undo);
292 if (currentPhoto != null)
294 // delete photo if necessary
297 _trackInfo.getPhotoList().deletePhoto(photoIndex);
301 // decouple photo from point
302 currentPhoto.setDataPoint(null);
306 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
313 * Delete the currently selected range
315 public void deleteSelectedRange()
319 // Find out if photos should be deleted or not
320 int selStart = _trackInfo.getSelection().getStart();
321 int selEnd = _trackInfo.getSelection().getEnd();
322 if (selStart >= 0 && selEnd >= selStart)
324 int numToDelete = selEnd - selStart + 1;
325 boolean[] deletePhotos = new boolean[numToDelete];
326 Photo[] photosToDelete = new Photo[numToDelete];
327 boolean deleteAll = false;
328 boolean deleteNone = false;
329 String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"),
330 I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"),
331 I18nManager.getText("button.cancel")};
332 DataPoint point = null;
333 for (int i=0; i<numToDelete; i++)
335 point = _trackInfo.getTrack().getPoint(i + selStart);
336 if (point != null && point.getPhoto() != null)
340 deletePhotos[i] = true;
341 photosToDelete[i] = point.getPhoto();
343 else if (deleteNone) {deletePhotos[i] = false;}
346 int response = JOptionPane.showOptionDialog(_frame,
347 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + point.getPhoto().getFile().getName(),
348 I18nManager.getText("dialog.deletepoint.title"),
349 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
350 questionOptions, questionOptions[1]);
351 // check for cancel or close
352 if (response == 4 || response == -1) {return;}
353 // check for yes or yes to all
354 if (response == 0 || response == 2)
356 deletePhotos[i] = true;
357 photosToDelete[i] = point.getPhoto();
358 if (response == 2) {deleteAll = true;}
360 // check for no to all
361 if (response == 3) {deleteNone = true;}
365 // add information to undo stack
366 UndoDeleteRange undo = new UndoDeleteRange(_trackInfo);
367 // delete requested photos
368 for (int i=0; i<numToDelete; i++)
370 point = _trackInfo.getTrack().getPoint(i + selStart);
371 if (point != null && point.getPhoto() != null)
375 // delete photo from list
376 _trackInfo.getPhotoList().deletePhoto(_trackInfo.getPhotoList().getPhotoIndex(point.getPhoto()));
380 // decouple from point
381 point.getPhoto().setDataPoint(null);
385 // call track to delete range
386 if (_trackInfo.deleteRange())
388 _undoStack.push(undo);
390 UpdateMessageBroker.informSubscribers("" + numToDelete + " "
391 + I18nManager.getText("confirm.deletepoint.multi"));
399 * Finish the compression by deleting the marked points
401 public void finishCompressTrack()
403 UndoCompress undo = new UndoCompress(_track);
404 // call track to do compress
405 int numPointsDeleted = _trackInfo.deleteMarkedPoints();
406 // add to undo stack if successful
407 if (numPointsDeleted > 0)
409 undo.setNumPointsDeleted(numPointsDeleted);
410 _undoStack.add(undo);
411 UpdateMessageBroker.informSubscribers("" + numPointsDeleted + " "
412 + (numPointsDeleted==1?I18nManager.getText("confirm.deletepoint.single"):I18nManager.getText("confirm.deletepoint.multi")));
415 showErrorMessage("function.compress", "dialog.compress.nonefound");
420 * Reverse the currently selected section of the track
422 public void reverseRange()
424 // check whether Timestamp field exists, and if so confirm reversal
425 int selStart = _trackInfo.getSelection().getStart();
426 int selEnd = _trackInfo.getSelection().getEnd();
427 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
428 || _mangleTimestampsConfirmed
429 || (JOptionPane.showConfirmDialog(_frame,
430 I18nManager.getText("dialog.confirmreversetrack.text"),
431 I18nManager.getText("dialog.confirmreversetrack.title"),
432 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
434 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
435 // call track to reverse range
436 if (_track.reverseRange(selStart, selEnd))
438 _undoStack.add(undo);
440 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
446 * Complete the add time offset function with the specified offset
447 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
449 public void finishAddTimeOffset(long inTimeOffset)
451 // Construct undo information
452 int selStart = _trackInfo.getSelection().getStart();
453 int selEnd = _trackInfo.getSelection().getEnd();
454 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
455 if (_trackInfo.getTrack().addTimeOffset(selStart, selEnd, inTimeOffset, false))
457 _undoStack.add(undo);
458 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
459 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
465 * Complete the add altitude offset function with the specified offset
466 * @param inOffset altitude offset to add as String
467 * @param inFormat altitude format of offset (eg Feet, Metres)
469 public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat)
472 if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) {
475 // Construct undo information
476 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
477 int selStart = _trackInfo.getSelection().getStart();
478 int selEnd = _trackInfo.getSelection().getEnd();
479 // How many decimal places are given in the offset?
480 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
481 boolean success = false;
482 // Decimal offset given
484 double offsetd = Double.parseDouble(inOffset);
485 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals);
487 catch (NumberFormatException nfe) {}
490 _undoStack.add(undo);
491 _trackInfo.getSelection().markInvalid();
492 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
493 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
499 * Merge the track segments within the current selection
501 public void mergeTrackSegments()
503 if (_trackInfo.getSelection().hasRangeSelected())
505 // Maybe could check segment start flags to see if it's worth merging
506 // If first track point is already start and no other seg starts then do nothing
508 int selStart = _trackInfo.getSelection().getStart();
509 int selEnd = _trackInfo.getSelection().getEnd();
511 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
512 // Call track to merge segments
513 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
514 _undoStack.add(undo);
515 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
522 * Interpolate the two selected points
524 public void interpolateSelection()
526 // Get number of points to add
527 Object numPointsStr = JOptionPane.showInputDialog(_frame,
528 I18nManager.getText("dialog.interpolate.parameter.text"),
529 I18nManager.getText("dialog.interpolate.title"),
530 JOptionPane.QUESTION_MESSAGE, null, null, "");
531 int numPoints = parseNumber(numPointsStr);
532 if (numPoints <= 0) return;
534 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1,
536 // call track to interpolate
537 if (_trackInfo.interpolate(numPoints))
539 _undoStack.add(undo);
545 * Average the selected points
547 public void averageSelection()
549 // Find following track point
550 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
551 boolean segFlag = false;
552 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
553 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
554 // call track info object to do the averaging
555 if (_trackInfo.average())
557 _undoStack.add(undo);
563 * Create a new point at the given position
564 * @param inPoint point to add
566 public void createPoint(DataPoint inPoint)
568 // create undo object
569 UndoCreatePoint undo = new UndoCreatePoint();
570 _undoStack.add(undo);
571 // add point to track
572 inPoint.setSegmentStart(true);
573 _track.appendPoints(new DataPoint[] {inPoint});
574 // ensure track's field list contains point's fields
575 _track.extendFieldList(inPoint.getFieldList());
576 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
578 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
583 * Cut the current selection and move it to before the currently selected point
585 public void cutAndMoveSelection()
587 int startIndex = _trackInfo.getSelection().getStart();
588 int endIndex = _trackInfo.getSelection().getEnd();
589 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
590 // If timestamps would be mangled by cut/move, confirm
591 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
592 || _mangleTimestampsConfirmed
593 || (JOptionPane.showConfirmDialog(_frame,
594 I18nManager.getText("dialog.confirmcutandmove.text"),
595 I18nManager.getText("dialog.confirmcutandmove.title"),
596 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
598 // Find points to set segment flags
599 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
600 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
601 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
603 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
604 // Call track info to move track section
605 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
607 // Set segment start flags (first track point, next track point, move to point)
608 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
609 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
610 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
612 // Add undo object to stack, set confirm message
613 _undoStack.add(undo);
614 _trackInfo.getSelection().selectRange(-1, -1);
615 UpdateMessageBroker.informSubscribers();
616 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
624 public void selectNone()
626 // deselect point, range and photo
627 _trackInfo.getSelection().clearAll();
628 _track.clearDeletionMarkers();
632 * Receive loaded data and start load
633 * @param inFieldArray array of fields
634 * @param inDataArray array of data
635 * @param inAltFormat altitude format
636 * @param inSourceInfo information about the source of the data
638 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
639 Altitude.Format inAltFormat, SourceInfo inSourceInfo)
641 informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo, null);
645 * Receive loaded data and determine whether to filter on tracks or not
646 * @param inFieldArray array of fields
647 * @param inDataArray array of data
648 * @param inAltFormat altitude format
649 * @param inSourceInfo information about the source of the data
650 * @param inTrackNameList information about the track names
652 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
653 Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
655 // Check whether loaded array can be properly parsed into a Track
656 Track loadedTrack = new Track();
657 loadedTrack.load(inFieldArray, inDataArray, inAltFormat);
658 if (loadedTrack.getNumPoints() <= 0)
660 showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
661 // load next file if there's a queue
665 // Check for doubled track
666 if (Checker.isDoubledTrack(loadedTrack)) {
667 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
668 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
670 // Look at TrackNameList, decide whether to filter or not
671 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
673 // Launch a dialog to let the user choose which tracks to load, then continue
674 new SelectTracksFunction(this, inFieldArray, inDataArray, inAltFormat, inSourceInfo,
675 inTrackNameList).begin();
678 // go directly to load
679 informDataLoaded(loadedTrack, inSourceInfo);
684 * Receive loaded data and optionally merge with current Track
685 * @param inLoadedTrack loaded track
686 * @param inSourceInfo information about the source of the data
688 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
690 // Decide whether to load or append
691 if (_track.getNumPoints() > 0)
693 // ask whether to replace or append
695 if (_dataFiles == null || _firstDataFile) {
696 answer = JOptionPane.showConfirmDialog(_frame,
697 I18nManager.getText("dialog.openappend.text"),
698 I18nManager.getText("dialog.openappend.title"),
699 JOptionPane.YES_NO_CANCEL_OPTION);
702 // Automatically append if there's a file load queue
703 answer = JOptionPane.YES_OPTION;
705 if (answer == JOptionPane.YES_OPTION)
707 // append data to current Track
708 _undoStack.add(new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints()));
709 _track.combine(inLoadedTrack);
710 // set source information
711 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
712 _trackInfo.getFileInfo().addSource(inSourceInfo);
714 else if (answer == JOptionPane.NO_OPTION)
716 // Don't append, replace data
717 PhotoList photos = null;
718 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
719 photos = _trackInfo.getPhotoList().cloneList();
721 _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos));
722 _lastSavePosition = _undoStack.size();
723 _trackInfo.getSelection().clearAll();
724 _track.load(inLoadedTrack);
725 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
726 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
727 if (photos != null) {
728 _trackInfo.getPhotoList().removeCorrelatedPhotos();
734 // Currently no data held, so transfer received data
735 _undoStack.add(new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null));
736 _lastSavePosition = _undoStack.size();
737 _trackInfo.getSelection().clearAll();
738 _track.load(inLoadedTrack);
739 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
740 _trackInfo.getFileInfo().addSource(inSourceInfo);
742 UpdateMessageBroker.informSubscribers();
744 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
745 + " '" + inSourceInfo.getName() + "'");
747 _menuManager.informFileLoaded();
748 // load next file if there's a queue
753 * Inform the app that NO data was loaded, eg cancel pressed
754 * Only needed if there's another file waiting in the queue
756 public void informNoDataLoaded()
758 // Load next file if there's a queue
763 * Load the next file in the waiting list, if any
765 private void loadNextFile()
767 _firstDataFile = false;
768 if (_dataFiles == null || _dataFiles.size() == 0) {
772 new Thread(new Runnable() {
774 File f = _dataFiles.get(0);
775 _dataFiles.remove(0);
776 _fileLoader.openFile(f);
784 * Accept a list of loaded photos
785 * @param inPhotoSet Set of Photo objects
787 public void informPhotosLoaded(Set<Photo> inPhotoSet)
789 if (inPhotoSet != null && !inPhotoSet.isEmpty())
791 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
792 int numPhotosAdded = numsAdded[0];
793 int numPointsAdded = numsAdded[1];
794 if (numPhotosAdded > 0)
796 // Save numbers so load can be undone
797 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
799 if (numPhotosAdded == 1) {
800 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
803 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
805 // MAYBE: Improve message when photo(s) fail to load (eg already added)
806 UpdateMessageBroker.informSubscribers();
808 _menuManager.informFileLoaded();
814 * Connect the current photo to the current point
816 public void connectPhotoToPoint()
818 Photo photo = _trackInfo.getCurrentPhoto();
819 DataPoint point = _trackInfo.getCurrentPoint();
820 if (photo != null && point != null)
822 if (point.getPhoto() == null)
824 // point doesn't currently have a photo, so just connect it
825 _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
826 photo.setDataPoint(point);
827 point.setPhoto(photo);
828 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
829 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.connect"));
836 * Disconnect the current photo from its point
838 public void disconnectPhotoFromPoint()
840 Photo photo = _trackInfo.getCurrentPhoto();
841 if (photo != null && photo.getDataPoint() != null)
843 DataPoint point = photo.getDataPoint();
844 _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
846 photo.setDataPoint(null);
847 point.setPhoto(null);
848 UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
849 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.photo.disconnect"));
855 * Remove the current photo, if any
857 public void deleteCurrentPhoto()
859 // Delete the current photo, and optionally its point too, keeping undo information
860 Photo currentPhoto = _trackInfo.getCurrentPhoto();
861 if (currentPhoto != null)
863 // Photo is selected, see if it has a point or not
864 boolean photoDeleted = false;
865 UndoDeletePhoto undoAction = null;
866 if (currentPhoto.getDataPoint() == null)
868 // no point attached, so just delete photo
869 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
871 photoDeleted = _trackInfo.deleteCurrentPhoto(false);
875 // point is attached, so need to confirm point deletion
876 undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(),
877 currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint()));
878 int response = JOptionPane.showConfirmDialog(_frame,
879 I18nManager.getText("dialog.deletephoto.deletepoint"),
880 I18nManager.getText("dialog.deletephoto.title"),
881 JOptionPane.YES_NO_CANCEL_OPTION);
882 boolean deletePointToo = (response == JOptionPane.YES_OPTION);
883 // Cancel delete if cancel pressed or dialog closed
884 if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION)
886 photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo);
889 // Add undo information to stack if necessary
892 _undoStack.add(undoAction);
899 * Save the coordinates of photos in their exif data
901 public void saveExif()
903 ExifSaver saver = new ExifSaver(_frame);
904 saver.saveExifInformation(_trackInfo.getPhotoList());
909 * Inform the app that the data has been saved
911 public void informDataSaved()
913 _lastSavePosition = _undoStack.size();
920 public void beginUndo()
922 if (_undoStack.isEmpty())
925 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
926 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
930 new UndoManager(this, _frame);
936 * Clear the undo stack (losing all undo information
938 public void clearUndo()
940 // Exit if nothing to undo
941 if (_undoStack == null || _undoStack.isEmpty())
943 // Has track got unsaved data?
944 boolean unsaved = hasDataUnsaved();
945 // Confirm operation with dialog
946 int answer = JOptionPane.showConfirmDialog(_frame,
947 I18nManager.getText("dialog.clearundo.text"),
948 I18nManager.getText("dialog.clearundo.title"),
949 JOptionPane.YES_NO_OPTION);
950 if (answer == JOptionPane.YES_OPTION)
953 _lastSavePosition = 0;
954 if (unsaved) _lastSavePosition = -1;
955 UpdateMessageBroker.informSubscribers();
961 * Undo the specified number of actions
962 * @param inNumUndos number of actions to undo
964 public void undoActions(int inNumUndos)
968 for (int i=0; i<inNumUndos; i++)
970 _undoStack.pop().performUndo(_trackInfo);
972 String message = "" + inNumUndos + " "
973 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
974 UpdateMessageBroker.informSubscribers(message);
976 catch (UndoException ue)
978 showErrorMessageNoLookup("error.undofailed.title",
979 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
981 UpdateMessageBroker.informSubscribers();
983 catch (EmptyStackException empty) {}
988 * Helper method to parse an Object into an integer
989 * @param inObject object, eg from dialog
990 * @return int value given
992 private static int parseNumber(Object inObject)
995 if (inObject != null)
999 num = Integer.parseInt(inObject.toString());
1001 catch (NumberFormatException nfe)
1008 * Show a map url in an external browser
1009 * @param inSourceIndex index of map source to use
1011 public void showExternalMap(int inSourceIndex)
1013 BrowserLauncher.launchBrowser(UrlGenerator.generateUrl(inSourceIndex, _trackInfo));
1017 * Display a standard error message
1018 * @param inTitleKey key to lookup for window title
1019 * @param inMessageKey key to lookup for error message
1021 public void showErrorMessage(String inTitleKey, String inMessageKey)
1023 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
1024 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1028 * Display a standard error message
1029 * @param inTitleKey key to lookup for window title
1030 * @param inMessage error message
1032 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
1034 JOptionPane.showMessageDialog(_frame, inMessage,
1035 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
1039 * @param inViewport viewport object
1041 public void setViewport(Viewport inViewport)
1043 _viewport = inViewport;
1047 * @return current viewport object
1049 public Viewport getViewport()
1055 * Set the controller for the full screen mode
1056 * @param inController controller object
1058 public void setSidebarController(SidebarController inController)
1060 __sidebarController = inController;
1064 * Toggle sidebars on and off
1066 public void toggleSidebars()
1068 __sidebarController.toggle();