4 import java.util.ArrayList;
5 import java.util.EmptyStackException;
8 import javax.swing.JFrame;
9 import javax.swing.JOptionPane;
11 import tim.prune.config.Config;
12 import tim.prune.data.Checker;
13 import tim.prune.data.DataPoint;
14 import tim.prune.data.Field;
15 import tim.prune.data.LatLonRectangle;
16 import tim.prune.data.NumberUtils;
17 import tim.prune.data.Photo;
18 import tim.prune.data.PhotoList;
19 import tim.prune.data.PointCreateOptions;
20 import tim.prune.data.RecentFile;
21 import tim.prune.data.SourceInfo;
22 import tim.prune.data.Track;
23 import tim.prune.data.TrackInfo;
24 import tim.prune.data.SourceInfo.FILE_TYPE;
25 import tim.prune.data.Unit;
26 import tim.prune.function.AsyncMediaLoader;
27 import tim.prune.function.SelectTracksFunction;
28 import tim.prune.function.edit.FieldEditList;
29 import tim.prune.function.edit.PointEditor;
30 import tim.prune.function.settings.SaveConfig;
31 import tim.prune.gui.MenuManager;
32 import tim.prune.gui.SidebarController;
33 import tim.prune.gui.UndoManager;
34 import tim.prune.gui.Viewport;
35 import tim.prune.gui.colour.ColourerCaretaker;
36 import tim.prune.gui.colour.PointColourer;
37 import tim.prune.load.FileLoader;
38 import tim.prune.load.JpegLoader;
39 import tim.prune.load.MediaLinkInfo;
40 import tim.prune.load.TrackNameList;
41 import tim.prune.save.ExifSaver;
42 import tim.prune.save.FileSaver;
43 import tim.prune.tips.TipManager;
44 import tim.prune.undo.*;
48 * Main controller for the application
53 private JFrame _frame = null;
54 private String _titlePrefix = null;
55 private Track _track = null;
56 private TrackInfo _trackInfo = null;
57 private int _lastSavePosition = 0;
58 private MenuManager _menuManager = null;
59 private SidebarController _sidebarController = null;
60 private FileLoader _fileLoader = null;
61 private JpegLoader _jpegLoader = null;
62 private FileSaver _fileSaver = null;
63 private UndoStack _undoStack = null;
64 private ColourerCaretaker _colCaretaker = null;
65 private boolean _mangleTimestampsConfirmed = false;
66 private Viewport _viewport = null;
67 private ArrayList<File> _dataFiles = null;
68 private boolean _autoAppendNextFile = false;
69 private boolean _busyLoading = false;
70 private AppMode _appMode = AppMode.NORMAL;
72 /** Enum for the app mode - currently only two options but may expand later */
73 public enum AppMode {NORMAL, DRAWRECT}
78 * @param inFrame frame object for application
80 public App(JFrame inFrame)
83 _titlePrefix = _frame.getTitle();
84 _undoStack = new UndoStack();
86 _trackInfo = new TrackInfo(_track);
87 FunctionLibrary.initialise(this);
88 _colCaretaker = new ColourerCaretaker(this);
89 UpdateMessageBroker.addSubscriber(_colCaretaker);
90 _colCaretaker.setColourer(Config.getPointColourer());
95 * @return the current TrackInfo
97 public TrackInfo getTrackInfo()
103 * @return the dialog frame
105 public JFrame getFrame()
111 * Check if the application has unsaved data
112 * @return true if data is unsaved, false otherwise
114 public boolean hasDataUnsaved()
116 return (_undoStack.size() > _lastSavePosition
117 && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia()));
121 * @return the undo stack
123 public UndoStack getUndoStack()
129 * Update the system's point colourer using the one in the Config
131 public void updatePointColourer()
133 if (_colCaretaker != null) {
134 _colCaretaker.setColourer(Config.getPointColourer());
139 * @return colourer object, or null
141 public PointColourer getPointColourer()
143 if (_colCaretaker == null) {return null;}
144 return _colCaretaker.getColourer();
148 * Show the specified tip if appropriate
149 * @param inTipNumber tip number from TipManager
151 public void showTip(int inTipNumber)
153 String key = TipManager.fireTipTrigger(inTipNumber);
154 if (key != null && !key.equals(""))
156 JOptionPane.showMessageDialog(_frame, I18nManager.getText(key),
157 I18nManager.getText("tip.title"), JOptionPane.INFORMATION_MESSAGE);
163 * Load the specified data files one by one
164 * @param inDataFiles arraylist containing File objects to load
166 public void loadDataFiles(ArrayList<File> inDataFiles)
168 if (inDataFiles == null || inDataFiles.size() == 0) {
173 _dataFiles = inDataFiles;
174 File f = _dataFiles.get(0);
175 _dataFiles.remove(0);
176 // Start load of specified file
177 if (_fileLoader == null)
178 _fileLoader = new FileLoader(this, _frame);
179 _autoAppendNextFile = false; // prompt for append
180 _fileLoader.openFile(f);
185 * Complete a function execution
186 * @param inUndo undo object to be added to stack
187 * @param inConfirmText confirmation text
189 public void completeFunction(UndoOperation inUndo, String inConfirmText)
191 _undoStack.add(inUndo);
192 UpdateMessageBroker.informSubscribers(inConfirmText);
193 setCurrentMode(AppMode.NORMAL);
197 * Set the MenuManager object to be informed about changes
198 * @param inManager MenuManager object
200 public void setMenuManager(MenuManager inManager)
202 _menuManager = inManager;
207 * Open a file containing track or waypoint data
209 public void openFile()
211 if (_fileLoader == null)
212 _fileLoader = new FileLoader(this, _frame);
213 _fileLoader.openFile();
218 * Add a photo or a directory of photos
220 public void addPhotos()
222 if (_jpegLoader == null)
223 _jpegLoader = new JpegLoader(this, _frame);
224 _jpegLoader.openDialog(new LatLonRectangle(_track.getLatRange(), _track.getLonRange()));
228 * Save the file in the selected format
230 public void saveFile()
232 if (_track == null) {
233 showErrorMessage("error.save.dialogtitle", "error.save.nodata");
237 if (_fileSaver == null) {
238 _fileSaver = new FileSaver(this, _frame);
241 if (_fileLoader != null) {delim = _fileLoader.getLastUsedDelimiter();}
242 _fileSaver.showDialog(delim);
248 * Exit the application if confirmed
254 _frame.requestFocus();
255 // check if ok to exit
256 Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")};
257 if (!hasDataUnsaved()
258 || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"),
259 I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION,
260 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
261 == JOptionPane.YES_OPTION)
264 if (Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS)) {
265 new SaveConfig(this).silentSave();
273 * Edit the currently selected point
275 public void editCurrentPoint()
279 DataPoint currentPoint = _trackInfo.getCurrentPoint();
280 if (currentPoint != null)
282 // Open point dialog to display details
283 PointEditor editor = new PointEditor(this, _frame);
284 editor.showDialog(_track, currentPoint);
291 * Complete the point edit
292 * @param inEditList field values to edit
293 * @param inUndoList field values before edit
295 public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
297 DataPoint currentPoint = _trackInfo.getCurrentPoint();
298 if (inEditList != null && inEditList.getNumEdits() > 0 && currentPoint != null)
300 // add information to undo stack
301 UndoOperation undo = new UndoEditPoint(currentPoint, inUndoList);
302 // pass to track for completion
303 if (_track.editPoint(currentPoint, inEditList, false))
305 _undoStack.add(undo);
306 // Confirm point edit
307 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.point.edit"));
314 * Delete the currently selected point
316 public void deleteCurrentPoint()
318 if (_track == null) {return;}
319 DataPoint currentPoint = _trackInfo.getCurrentPoint();
320 if (currentPoint != null)
323 boolean deletePhoto = false;
324 Photo currentPhoto = currentPoint.getPhoto();
325 if (currentPhoto != null)
327 // Confirm deletion of photo or decoupling
328 int response = JOptionPane.showConfirmDialog(_frame,
329 I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getName(),
330 I18nManager.getText("dialog.deletepoint.title"),
331 JOptionPane.YES_NO_CANCEL_OPTION);
332 if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION)
334 // cancel pressed- abort delete
337 if (response == JOptionPane.YES_OPTION) {deletePhoto = true;}
339 // store necessary information to undo it later
340 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
341 int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto);
342 int audioIndex = _trackInfo.getAudioList().getAudioIndex(currentPoint.getAudio());
343 DataPoint nextTrackPoint = _trackInfo.getTrack().getNextTrackPoint(pointIndex + 1);
344 // Construct Undo object
345 UndoDeletePoint undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex,
346 audioIndex, nextTrackPoint != null && nextTrackPoint.getSegmentStart());
347 undo.setAtBoundaryOfSelectedRange(pointIndex == _trackInfo.getSelection().getStart() ||
348 pointIndex == _trackInfo.getSelection().getEnd());
349 // call track to delete point
350 if (_trackInfo.deletePoint())
352 // Delete was successful so add undo info to stack
353 _undoStack.add(undo);
354 if (currentPhoto != null)
356 // delete photo if necessary
359 _trackInfo.getPhotoList().deletePhoto(photoIndex);
363 // decouple photo from point
364 currentPhoto.setDataPoint(null);
366 UpdateMessageBroker.informSubscribers(DataSubscriber.PHOTOS_MODIFIED);
368 // Delete audio object (without bothering to ask)
369 if (audioIndex > -1) {
370 _trackInfo.getAudioList().deleteAudio(audioIndex);
373 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.deletepoint.single"));
374 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_ADDED_OR_REMOVED);
381 * Reverse the currently selected section of the track
383 public void reverseRange()
385 // check whether Timestamp field exists, and if so confirm reversal
386 int selStart = _trackInfo.getSelection().getStart();
387 int selEnd = _trackInfo.getSelection().getEnd();
388 if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd)
389 || _mangleTimestampsConfirmed
390 || (JOptionPane.showConfirmDialog(_frame,
391 I18nManager.getText("dialog.confirmreversetrack.text"),
392 I18nManager.getText("dialog.confirmreversetrack.title"),
393 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
395 UndoReverseSection undo = new UndoReverseSection(_track, selStart, selEnd);
396 // call track to reverse range
397 if (_track.reverseRange(selStart, selEnd))
399 _undoStack.add(undo);
401 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.reverserange"));
407 * Complete the add time offset function with the specified offset in seconds
408 * @param inTimeOffset time offset to add (+ve for add, -ve for subtract)
410 public void finishAddTimeOffsetSeconds(long inTimeOffset)
412 // Construct undo information
413 int selStart = _trackInfo.getSelection().getStart();
414 int selEnd = _trackInfo.getSelection().getEnd();
415 UndoAddTimeOffset undo = new UndoAddTimeOffset(selStart, selEnd, inTimeOffset);
416 if (_trackInfo.getTrack().addTimeOffsetSeconds(selStart, selEnd, inTimeOffset, false))
418 _undoStack.add(undo);
419 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
420 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addtimeoffset"));
426 * Complete the add altitude offset function with the specified offset
427 * @param inOffset altitude offset to add as String
428 * @param inUnit altitude units of offset (eg Feet, Metres)
430 public void finishAddAltitudeOffset(String inOffset, Unit inUnit)
433 if (inOffset == null || inOffset.equals("") || inUnit == null) {
436 // Construct undo information
437 UndoAddAltitudeOffset undo = new UndoAddAltitudeOffset(_trackInfo);
438 int selStart = _trackInfo.getSelection().getStart();
439 int selEnd = _trackInfo.getSelection().getEnd();
440 // How many decimal places are given in the offset?
441 int numDecimals = NumberUtils.getDecimalPlaces(inOffset);
442 boolean success = false;
443 // Decimal offset given
445 double offsetd = Double.parseDouble(inOffset);
446 success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals);
448 catch (NumberFormatException nfe) {}
451 _undoStack.add(undo);
452 _trackInfo.getSelection().markInvalid();
453 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
454 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.addaltitudeoffset"));
460 * Remove altitudes from selected points
462 public void removeAltitudes(int selStart, int selEnd)
464 UndoRemoveAltitudes undo = new UndoRemoveAltitudes(_trackInfo, selStart, selEnd);
465 if (_trackInfo.getTrack().removeAltitudes(selStart, selEnd))
467 _undoStack.add(undo);
468 _trackInfo.getSelection().markInvalid();
469 UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
470 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.removealtitudes"));
476 * Merge the track segments within the current selection
478 public void mergeTrackSegments()
480 if (_trackInfo.getSelection().hasRangeSelected())
482 // Maybe could check segment start flags to see if it's worth merging
483 // If first track point is already start and no other seg starts then do nothing
485 int selStart = _trackInfo.getSelection().getStart();
486 int selEnd = _trackInfo.getSelection().getEnd();
488 UndoMergeTrackSegments undo = new UndoMergeTrackSegments(_track, selStart, selEnd);
489 // Call track to merge segments
490 if (_trackInfo.mergeTrackSegments(selStart, selEnd)) {
491 _undoStack.add(undo);
492 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.mergetracksegments"));
499 * Average the selected points
501 public void averageSelection()
503 // Find following track point
504 DataPoint nextPoint = _track.getNextTrackPoint(_trackInfo.getSelection().getEnd() + 1);
505 boolean segFlag = false;
506 if (nextPoint != null) {segFlag = nextPoint.getSegmentStart();}
507 UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getEnd() + 1, 1, nextPoint != null, segFlag);
508 // call track info object to do the averaging
509 if (_trackInfo.average())
511 _undoStack.add(undo);
517 * Create a new point at the end of the track
518 * @param inPoint point to add
520 public void createPoint(DataPoint inPoint)
522 createPoint(inPoint, true);
526 * Create a new point at the end of the track
527 * @param inPoint point to add
528 * @param inNewSegment true for a single point, false for a continuation
530 public void createPoint(DataPoint inPoint, boolean inNewSegment)
532 // create undo object
533 UndoCreatePoint undo = new UndoCreatePoint();
534 _undoStack.add(undo);
535 // add point to track
536 inPoint.setSegmentStart(inNewSegment);
537 _track.appendPoints(new DataPoint[] {inPoint});
538 // ensure track's field list contains point's fields
539 _track.extendFieldList(inPoint.getFieldList());
540 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
542 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
547 * Create a new point before the given position
548 * @param inPoint point to add
549 * @param inIndex index of following point
551 public void createPoint(DataPoint inPoint, int inIndex)
553 // create undo object
554 UndoInsert undo = new UndoInsert(inIndex, 1);
555 _undoStack.add(undo);
556 // add point to track
557 _track.insertPoint(inPoint, inIndex);
558 // ensure track's field list contains point's fields
559 _track.extendFieldList(inPoint.getFieldList());
560 _trackInfo.selectPoint(inIndex);
561 final int selStart = _trackInfo.getSelection().getStart();
562 final int selEnd = _trackInfo.getSelection().getEnd();
563 if (selStart < inIndex && selEnd >= inIndex)
565 // Extend end of selection by 1
566 _trackInfo.getSelection().selectRange(selStart, selEnd+1);
569 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.createpoint"));
574 * Cut the current selection and move it to before the currently selected point
576 public void cutAndMoveSelection()
578 int startIndex = _trackInfo.getSelection().getStart();
579 int endIndex = _trackInfo.getSelection().getEnd();
580 int pointIndex = _trackInfo.getSelection().getCurrentPointIndex();
581 // If timestamps would be mangled by cut/move, confirm
582 if (!_track.hasData(Field.TIMESTAMP, startIndex, endIndex)
583 || _mangleTimestampsConfirmed
584 || (JOptionPane.showConfirmDialog(_frame,
585 I18nManager.getText("dialog.confirmcutandmove.text"),
586 I18nManager.getText("dialog.confirmcutandmove.title"),
587 JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_mangleTimestampsConfirmed = true)))
589 // Find points to set segment flags
590 DataPoint firstTrackPoint = _track.getNextTrackPoint(startIndex, endIndex);
591 DataPoint nextTrackPoint = _track.getNextTrackPoint(endIndex+1);
592 DataPoint moveToTrackPoint = _track.getNextTrackPoint(pointIndex);
594 UndoCutAndMove undo = new UndoCutAndMove(_track, startIndex, endIndex, pointIndex);
595 // Call track info to move track section
596 if (_track.cutAndMoveSection(startIndex, endIndex, pointIndex))
598 // Set segment start flags (first track point, next track point, move to point)
599 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
600 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
601 if (moveToTrackPoint != null) {moveToTrackPoint.setSegmentStart(true);}
603 // Add undo object to stack, set confirm message
604 _undoStack.add(undo);
605 _trackInfo.getSelection().selectRange(-1, -1);
606 UpdateMessageBroker.informSubscribers();
607 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.cutandmove"));
615 public void selectNone()
617 // deselect point, range and photo
618 _trackInfo.getSelection().clearAll();
619 _track.clearDeletionMarkers();
623 * Receive loaded data and determine whether to filter on tracks or not
624 * @param inFieldArray array of fields
625 * @param inDataArray array of data
626 * @param inSourceInfo information about the source of the data
627 * @param inTrackNameList information about the track names
629 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
630 SourceInfo inSourceInfo, TrackNameList inTrackNameList)
632 // no link array given
633 informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo,
634 inTrackNameList, null);
638 * Receive loaded data and determine whether to filter on tracks or not
639 * @param inFieldArray array of fields
640 * @param inDataArray array of data
641 * @param inOptions creation options such as units
642 * @param inSourceInfo information about the source of the data
643 * @param inTrackNameList information about the track names
645 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray,
646 PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList)
648 // no link array given
649 informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo,
650 inTrackNameList, null);
654 * Receive loaded data and determine whether to filter on tracks or not
655 * @param inFieldArray array of fields
656 * @param inDataArray array of data
657 * @param inOptions creation options such as units
658 * @param inSourceInfo information about the source of the data
659 * @param inTrackNameList information about the track names
660 * @param inLinkInfo links to photo/audio clips
662 public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions,
663 SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo)
665 // Check whether loaded array can be properly parsed into a Track
666 Track loadedTrack = new Track();
667 loadedTrack.load(inFieldArray, inDataArray, inOptions);
668 if (loadedTrack.getNumPoints() <= 0)
670 String msgKey = (inSourceInfo == null ? "error.load.nopointsintext" : "error.load.nopoints");
671 showErrorMessage("error.load.dialogtitle", msgKey);
672 // load next file if there's a queue
676 // Check for doubled track
677 if (Checker.isDoubledTrack(loadedTrack)) {
678 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.open.contentsdoubled"),
679 I18nManager.getText("function.open"), JOptionPane.WARNING_MESSAGE);
683 // Attach photos and/or audio clips to points
684 if (inLinkInfo != null)
686 String[] linkArray = inLinkInfo.getLinkArray();
687 if (linkArray != null) {
688 new AsyncMediaLoader(this, inLinkInfo.getZipFile(), linkArray, loadedTrack, inSourceInfo.getFile()).begin();
691 // Look at TrackNameList, decide whether to filter or not
692 if (inTrackNameList != null && inTrackNameList.getNumTracks() > 1)
694 // Launch a dialog to let the user choose which tracks to load, then continue
695 new SelectTracksFunction(this, loadedTrack, inSourceInfo, inTrackNameList).begin();
698 // go directly to load
699 informDataLoaded(loadedTrack, inSourceInfo);
701 setCurrentMode(AppMode.NORMAL);
706 * Receive loaded data and optionally merge with current Track
707 * @param inLoadedTrack loaded track
708 * @param inSourceInfo information about the source of the data
710 public void informDataLoaded(Track inLoadedTrack, SourceInfo inSourceInfo)
712 // Decide whether to load or append
713 if (_track.getNumPoints() > 0)
715 // ask whether to replace or append
717 if (_autoAppendNextFile) {
718 // Automatically append the next file
719 answer = JOptionPane.YES_OPTION;
722 // Ask whether to append or not
723 answer = JOptionPane.showConfirmDialog(_frame,
724 I18nManager.getText("dialog.openappend.text"),
725 I18nManager.getText("dialog.openappend.title"),
726 JOptionPane.YES_NO_CANCEL_OPTION);
728 _autoAppendNextFile = false; // reset flag to cancel autoappend
730 if (answer == JOptionPane.YES_OPTION)
732 // append data to current Track
733 UndoLoad undo = new UndoLoad(_track.getNumPoints(), inLoadedTrack.getNumPoints());
734 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
735 _undoStack.add(undo);
736 _track.combine(inLoadedTrack);
737 if (inSourceInfo != null)
739 // set source information
740 inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
741 _trackInfo.getFileInfo().addSource(inSourceInfo);
744 else if (answer == JOptionPane.NO_OPTION)
746 // Don't append, replace data
747 PhotoList photos = null;
748 if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) {
749 photos = _trackInfo.getPhotoList().cloneList();
751 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), photos);
752 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
753 _undoStack.add(undo);
754 _lastSavePosition = _undoStack.size();
755 _trackInfo.getSelection().clearAll();
756 _track.load(inLoadedTrack);
757 if (inSourceInfo != null)
759 // set source information
760 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
761 _trackInfo.getFileInfo().replaceSource(inSourceInfo);
763 _trackInfo.getPhotoList().removeCorrelatedPhotos();
764 _trackInfo.getAudioList().removeCorrelatedAudios();
769 // Currently no data held, so transfer received data
770 UndoLoad undo = new UndoLoad(_trackInfo, inLoadedTrack.getNumPoints(), null);
771 undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
772 _undoStack.add(undo);
773 _lastSavePosition = _undoStack.size();
774 _trackInfo.getSelection().clearAll();
775 _track.load(inLoadedTrack);
776 if (inSourceInfo != null)
778 inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
779 _trackInfo.getFileInfo().addSource(inSourceInfo);
782 // Update config before subscribers are told
783 if (inSourceInfo != null)
785 boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
786 Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
788 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
789 + " '" + inSourceInfo.getName() + "'");
791 UpdateMessageBroker.informSubscribers();
793 _menuManager.informFileLoaded();
794 // recentre viewport on new file data
795 _viewport.recentreViewport();
796 // update main window title
799 _busyLoading = false;
800 // load next file if there's a queue
805 * Inform the app that NO data was loaded, eg cancel pressed
806 * Only needed if there's another file waiting in the queue
808 public void informNoDataLoaded()
810 // Load next file if there's a queue
815 * External trigger to automatically append the next loaded file
816 * instead of prompting to replace or append
818 public void autoAppendNextFile()
820 _autoAppendNextFile = true;
824 * Load the next file in the waiting list, if any
826 private void loadNextFile()
828 if (_dataFiles == null || _dataFiles.size() == 0) {
832 new Thread(new Runnable() {
834 File f = _dataFiles.get(0);
835 _dataFiles.remove(0);
836 _autoAppendNextFile = true;
837 _fileLoader.openFile(f);
845 * Accept a list of loaded photos
846 * @param inPhotoSet Set of Photo objects
848 public void informPhotosLoaded(Set<Photo> inPhotoSet)
850 if (inPhotoSet != null && !inPhotoSet.isEmpty())
852 int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
853 int numPhotosAdded = numsAdded[0];
854 int numPointsAdded = numsAdded[1];
855 if (numPhotosAdded > 0)
857 // Save numbers so load can be undone
858 _undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
860 if (numPhotosAdded == 1) {
861 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.single"));
864 UpdateMessageBroker.informSubscribers("" + numPhotosAdded + " " + I18nManager.getText("confirm.jpegload.multi"));
866 // MAYBE: Improve message when photo(s) fail to load (eg already added)
867 UpdateMessageBroker.informSubscribers();
869 if (numPointsAdded > 0) _menuManager.informFileLoaded();
875 * Save the coordinates of photos in their exif data
877 public void saveExif()
879 ExifSaver saver = new ExifSaver(_frame);
880 saver.saveExifInformation(_trackInfo.getPhotoList());
885 * Inform the app that the data has been saved
887 public void informDataSaved()
889 _lastSavePosition = _undoStack.size();
896 public void beginUndo()
898 if (_undoStack.isEmpty())
901 JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"),
902 I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE);
906 new UndoManager(this, _frame).show();
912 * Clear the undo stack (losing all undo information
914 public void clearUndo()
916 // Exit if nothing to undo
917 if (_undoStack == null || _undoStack.isEmpty())
919 // Has track got unsaved data?
920 boolean unsaved = hasDataUnsaved();
921 // Confirm operation with dialog
922 int answer = JOptionPane.showConfirmDialog(_frame,
923 I18nManager.getText("dialog.clearundo.text"),
924 I18nManager.getText("dialog.clearundo.title"),
925 JOptionPane.YES_NO_OPTION);
926 if (answer == JOptionPane.YES_OPTION)
929 _lastSavePosition = 0;
930 if (unsaved) _lastSavePosition = -1;
931 UpdateMessageBroker.informSubscribers();
937 * Undo the specified number of actions
938 * @param inNumUndos number of actions to undo
940 public void undoActions(int inNumUndos)
944 for (int i=0; i<inNumUndos; i++)
946 _undoStack.popOperation().performUndo(_trackInfo);
948 String message = "" + inNumUndos + " "
949 + (inNumUndos==1?I18nManager.getText("confirm.undo.single"):I18nManager.getText("confirm.undo.multi"));
950 UpdateMessageBroker.informSubscribers(message);
952 catch (UndoException ue)
954 showErrorMessageNoLookup("error.undofailed.title",
955 I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage());
958 catch (EmptyStackException empty) {}
959 UpdateMessageBroker.informSubscribers();
963 * @return the current data status, used for later comparison
965 public DataStatus getCurrentDataStatus()
967 return new DataStatus(_undoStack.size(), _undoStack.getNumUndos());
972 * Display a standard error message
973 * @param inTitleKey key to lookup for window title
974 * @param inMessageKey key to lookup for error message
976 public void showErrorMessage(String inTitleKey, String inMessageKey)
978 JOptionPane.showMessageDialog(_frame, I18nManager.getText(inMessageKey),
979 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
983 * Display a standard error message
984 * @param inTitleKey key to lookup for window title
985 * @param inMessage error message
987 public void showErrorMessageNoLookup(String inTitleKey, String inMessage)
989 JOptionPane.showMessageDialog(_frame, inMessage,
990 I18nManager.getText(inTitleKey), JOptionPane.ERROR_MESSAGE);
994 * @param inViewport viewport object
996 public void setViewport(Viewport inViewport)
998 _viewport = inViewport;
1002 * @return current viewport object
1004 public Viewport getViewport()
1010 * Set the controller for the full screen mode
1011 * @param inController controller object
1013 public void setSidebarController(SidebarController inController)
1015 _sidebarController = inController;
1019 * Toggle sidebars on and off
1021 public void toggleSidebars()
1023 _sidebarController.toggle();
1026 /** @return true if App is currently busy with loading data */
1027 public boolean isBusyLoading() {
1028 return _busyLoading;
1031 /** @return current app mode */
1032 public AppMode getCurrentMode() {
1036 /** @param inMode the current app mode */
1037 public void setCurrentMode(AppMode inMode) {
1041 /** Update main window title **/
1042 public void updateTitle() {
1043 ArrayList<String> filenames = _trackInfo.getFileInfo().getFilenames();
1044 if (filenames.size() > 0) {
1045 _frame.setTitle(_titlePrefix + ": " + String.join(", ", filenames));
1049 _frame.setTitle(_titlePrefix);