From 312fec956e43f5d0a38617da5d0add9c62563e2c Mon Sep 17 00:00:00 2001 From: activityworkshop Date: Sat, 14 Feb 2015 14:29:47 +0100 Subject: [PATCH] Version 1, September 2006 --- tim/prune/App.java | 531 +++++++++++++ tim/prune/DataSubscriber.java | 14 + tim/prune/GpsPruner.java | 95 +++ tim/prune/I18nManager.java | 76 ++ tim/prune/UpdateMessageBroker.java | 49 ++ tim/prune/data/Altitude.java | 113 +++ tim/prune/data/AltitudeRange.java | 65 ++ tim/prune/data/Coordinate.java | 279 +++++++ tim/prune/data/DataPoint.java | 213 +++++ tim/prune/data/Distance.java | 36 + tim/prune/data/DoubleRange.java | 50 ++ tim/prune/data/Field.java | 90 +++ tim/prune/data/FieldList.java | 152 ++++ tim/prune/data/FieldType.java | 26 + tim/prune/data/FileInfo.java | 60 ++ tim/prune/data/IntegerRange.java | 51 ++ tim/prune/data/Latitude.java | 64 ++ tim/prune/data/Longitude.java | 64 ++ tim/prune/data/Selection.java | 404 ++++++++++ tim/prune/data/Timestamp.java | 150 ++++ tim/prune/data/Track.java | 793 +++++++++++++++++++ tim/prune/data/TrackInfo.java | 162 ++++ tim/prune/gui/AboutScreen.java | 90 +++ tim/prune/gui/DetailsDisplay.java | 449 +++++++++++ tim/prune/gui/GenericChart.java | 103 +++ tim/prune/gui/GenericDisplay.java | 26 + tim/prune/gui/MapChart.java | 581 ++++++++++++++ tim/prune/gui/MenuManager.java | 337 ++++++++ tim/prune/gui/ProfileChart.java | 207 +++++ tim/prune/gui/UndoManager.java | 95 +++ tim/prune/lang/prune-texts.properties | 187 +++++ tim/prune/lang/prune-texts_de.properties | 187 +++++ tim/prune/lang/prune-texts_de_CH.properties | 187 +++++ tim/prune/license.txt | 339 ++++++++ tim/prune/load/DelimiterInfo.java | 63 ++ tim/prune/load/FieldSelectionTableModel.java | 240 ++++++ tim/prune/load/FileCacher.java | 125 +++ tim/prune/load/FileExtractTableModel.java | 81 ++ tim/prune/load/FileLoader.java | 563 +++++++++++++ tim/prune/load/FileSplitter.java | 147 ++++ tim/prune/load/OneCharDocument.java | 21 + tim/prune/readme.txt | 37 + tim/prune/save/FieldInfo.java | 69 ++ tim/prune/save/FieldSelectionTableModel.java | 142 ++++ tim/prune/save/FileSaver.java | 461 +++++++++++ tim/prune/save/KmlExporter.java | 207 +++++ tim/prune/save/UpDownToggler.java | 52 ++ tim/prune/undo/UndoCompress.java | 61 ++ tim/prune/undo/UndoDeleteDuplicates.java | 32 + tim/prune/undo/UndoDeletePoint.java | 54 ++ tim/prune/undo/UndoDeleteRange.java | 47 ++ tim/prune/undo/UndoException.java | 16 + tim/prune/undo/UndoInsert.java | 47 ++ tim/prune/undo/UndoLoad.java | 88 ++ tim/prune/undo/UndoOperation.java | 21 + tim/prune/undo/UndoRearrangeWaypoints.java | 44 + tim/prune/undo/UndoReverseSection.java | 46 ++ 57 files changed, 8989 insertions(+) create mode 100644 tim/prune/App.java create mode 100644 tim/prune/DataSubscriber.java create mode 100644 tim/prune/GpsPruner.java create mode 100644 tim/prune/I18nManager.java create mode 100644 tim/prune/UpdateMessageBroker.java create mode 100644 tim/prune/data/Altitude.java create mode 100644 tim/prune/data/AltitudeRange.java create mode 100644 tim/prune/data/Coordinate.java create mode 100644 tim/prune/data/DataPoint.java create mode 100644 tim/prune/data/Distance.java create mode 100644 tim/prune/data/DoubleRange.java create mode 100644 tim/prune/data/Field.java create mode 100644 tim/prune/data/FieldList.java create mode 100644 tim/prune/data/FieldType.java create mode 100644 tim/prune/data/FileInfo.java create mode 100644 tim/prune/data/IntegerRange.java create mode 100644 tim/prune/data/Latitude.java create mode 100644 tim/prune/data/Longitude.java create mode 100644 tim/prune/data/Selection.java create mode 100644 tim/prune/data/Timestamp.java create mode 100644 tim/prune/data/Track.java create mode 100644 tim/prune/data/TrackInfo.java create mode 100644 tim/prune/gui/AboutScreen.java create mode 100644 tim/prune/gui/DetailsDisplay.java create mode 100644 tim/prune/gui/GenericChart.java create mode 100644 tim/prune/gui/GenericDisplay.java create mode 100644 tim/prune/gui/MapChart.java create mode 100644 tim/prune/gui/MenuManager.java create mode 100644 tim/prune/gui/ProfileChart.java create mode 100644 tim/prune/gui/UndoManager.java create mode 100644 tim/prune/lang/prune-texts.properties create mode 100644 tim/prune/lang/prune-texts_de.properties create mode 100644 tim/prune/lang/prune-texts_de_CH.properties create mode 100644 tim/prune/license.txt create mode 100644 tim/prune/load/DelimiterInfo.java create mode 100644 tim/prune/load/FieldSelectionTableModel.java create mode 100644 tim/prune/load/FileCacher.java create mode 100644 tim/prune/load/FileExtractTableModel.java create mode 100644 tim/prune/load/FileLoader.java create mode 100644 tim/prune/load/FileSplitter.java create mode 100644 tim/prune/load/OneCharDocument.java create mode 100644 tim/prune/readme.txt create mode 100644 tim/prune/save/FieldInfo.java create mode 100644 tim/prune/save/FieldSelectionTableModel.java create mode 100644 tim/prune/save/FileSaver.java create mode 100644 tim/prune/save/KmlExporter.java create mode 100644 tim/prune/save/UpDownToggler.java create mode 100644 tim/prune/undo/UndoCompress.java create mode 100644 tim/prune/undo/UndoDeleteDuplicates.java create mode 100644 tim/prune/undo/UndoDeletePoint.java create mode 100644 tim/prune/undo/UndoDeleteRange.java create mode 100644 tim/prune/undo/UndoException.java create mode 100644 tim/prune/undo/UndoInsert.java create mode 100644 tim/prune/undo/UndoLoad.java create mode 100644 tim/prune/undo/UndoOperation.java create mode 100644 tim/prune/undo/UndoRearrangeWaypoints.java create mode 100644 tim/prune/undo/UndoReverseSection.java diff --git a/tim/prune/App.java b/tim/prune/App.java new file mode 100644 index 0000000..a1e6a8b --- /dev/null +++ b/tim/prune/App.java @@ -0,0 +1,531 @@ +package tim.prune; + +import java.util.EmptyStackException; +import java.util.Stack; + +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +import tim.prune.data.DataPoint; +import tim.prune.data.Field; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; +import tim.prune.gui.MenuManager; +import tim.prune.gui.UndoManager; +import tim.prune.load.FileLoader; +import tim.prune.save.FileSaver; +import tim.prune.save.KmlExporter; +import tim.prune.undo.UndoCompress; +import tim.prune.undo.UndoDeleteDuplicates; +import tim.prune.undo.UndoDeletePoint; +import tim.prune.undo.UndoDeleteRange; +import tim.prune.undo.UndoException; +import tim.prune.undo.UndoInsert; +import tim.prune.undo.UndoLoad; +import tim.prune.undo.UndoOperation; +import tim.prune.undo.UndoRearrangeWaypoints; +import tim.prune.undo.UndoReverseSection; + + +/** + * Main controller for the application + */ +public class App +{ + // Instance variables + private JFrame _frame = null; + private Track _track = null; + private TrackInfo _trackInfo = null; + private int _lastSavePosition = 0; + private MenuManager _menuManager = null; + private FileLoader _fileLoader = null; + private Stack _undoStack = null; + private UpdateMessageBroker _broker = null; + private boolean _reversePointsConfirmed = false; + + // Constants + public static final int REARRANGE_TO_START = 0; + public static final int REARRANGE_TO_END = 1; + public static final int REARRANGE_TO_NEAREST = 2; + + + // TODO: Make waypoint window to allow list of waypoints, edit names etc + + /** + * Constructor + * @param inFrame frame object for application + * @param inBroker message broker + */ + public App(JFrame inFrame, UpdateMessageBroker inBroker) + { + _frame = inFrame; + _undoStack = new Stack(); + _broker = inBroker; + _track = new Track(_broker); + _trackInfo = new TrackInfo(_track, _broker); + } + + + /** + * @return the current TrackInfo + */ + public TrackInfo getTrackInfo() + { + return _trackInfo; + } + + /** + * Check if the application has unsaved data + * @return true if data is unsaved, false otherwise + */ + public boolean hasDataUnsaved() + { + return _undoStack.size() > _lastSavePosition; + } + + /** + * @return the undo stack + */ + public Stack getUndoStack() + { + return _undoStack; + } + + /** + * Set the MenuManager object to be informed about changes + * @param inManager MenuManager object + */ + public void setMenuManager(MenuManager inManager) + { + _menuManager = inManager; + } + + + /** + * Open a file containing track or waypoint data + */ + public void openFile() + { + if (_fileLoader == null) + _fileLoader = new FileLoader(this, _frame); + _fileLoader.openFile(); + } + + + /** + * Save the file in the selected format + */ + public void saveFile() + { + if (_track == null) + { + JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"), + I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else + { + FileSaver saver = new FileSaver(this, _frame, _track); + saver.showDialog(_fileLoader.getLastUsedDelimiter()); + } + } + + + /** + * Export track data as Kml + */ + public void exportKml() + { + if (_track == null) + { + JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"), + I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + else + { + KmlExporter exporter = new KmlExporter(this, _frame, _track); + exporter.showDialog(); + } + } + + + /** + * Exit the application if confirmed + */ + public void exit() + { + // check if ok to exit + Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")}; + if (!hasDataUnsaved() + || JOptionPane.showOptionDialog(_frame, I18nManager.getText("dialog.exit.confirm.text"), + I18nManager.getText("dialog.exit.confirm.title"), JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) + == JOptionPane.YES_OPTION) + { + System.exit(0); + } + } + + + /** + * Delete the currently selected point + */ + public void deleteCurrentPoint() + { + if (_track != null) + { + DataPoint currentPoint = _trackInfo.getCurrentPoint(); + if (currentPoint != null) + { + // add information to undo stack + int pointIndex = _trackInfo.getSelection().getCurrentPointIndex(); + UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint); + // call track to delete point + if (_trackInfo.deletePoint()) + { + _undoStack.push(undo); + } + } + } + } + + + /** + * Delete the currently selected range + */ + public void deleteSelectedRange() + { + if (_track != null) + { + // add information to undo stack + UndoOperation undo = new UndoDeleteRange(_trackInfo); + // call track to delete point + if (_trackInfo.deleteRange()) + { + _undoStack.push(undo); + } + } + } + + + /** + * Delete all the duplicate points in the track + */ + public void deleteDuplicates() + { + if (_track != null) + { + // Save undo information + UndoOperation undo = new UndoDeleteDuplicates(_track); + // tell track to do it + int numDeleted = _trackInfo.deleteDuplicates(); + if (numDeleted > 0) + { + _undoStack.add(undo); + String message = null; + if (numDeleted == 1) + { + message = "1 " + I18nManager.getText("dialog.deleteduplicates.single.text"); + } + else + { + message = "" + numDeleted + " " + I18nManager.getText("dialog.deleteduplicates.multi.text"); + } + JOptionPane.showMessageDialog(_frame, message, + I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE); + } + else + { + JOptionPane.showMessageDialog(_frame, + I18nManager.getText("dialog.deleteduplicates.nonefound"), + I18nManager.getText("dialog.deleteduplicates.title"), JOptionPane.INFORMATION_MESSAGE); + } + } + } + + + /** + * Compress the track + */ + public void compressTrack() + { + UndoCompress undo = new UndoCompress(_track); + // Get compression parameter + Object compParam = JOptionPane.showInputDialog(_frame, + I18nManager.getText("dialog.compresstrack.parameter.text"), + I18nManager.getText("dialog.compresstrack.title"), + JOptionPane.QUESTION_MESSAGE, null, null, "100"); + int compNumber = parseNumber(compParam); + if (compNumber <= 0) return; + // call track to do compress + int numPointsDeleted = _trackInfo.compress(compNumber); + // add to undo stack if successful + if (numPointsDeleted > 0) + { + undo.setNumPointsDeleted(numPointsDeleted); + _undoStack.add(undo); + JOptionPane.showMessageDialog(_frame, + I18nManager.getText("dialog.compresstrack.text") + " - " + + numPointsDeleted + " " + + (numPointsDeleted==1?I18nManager.getText("dialog.compresstrack.single.text"):I18nManager.getText("dialog.compresstrack.multi.text")), + I18nManager.getText("dialog.compresstrack.title"), JOptionPane.INFORMATION_MESSAGE); + } + else + { + JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.compresstrack.nonefound"), + I18nManager.getText("dialog.compresstrack.title"), JOptionPane.WARNING_MESSAGE); + } + } + + + /** + * Reverse a section of the track + */ + public void reverseRange() + { + // check whether Timestamp field exists, and if so confirm reversal + int selStart = _trackInfo.getSelection().getStart(); + int selEnd = _trackInfo.getSelection().getEnd(); + if (!_track.hasData(Field.TIMESTAMP, selStart, selEnd) + || _reversePointsConfirmed + || (JOptionPane.showConfirmDialog(_frame, + I18nManager.getText("dialog.confirmreversetrack.text"), + I18nManager.getText("dialog.confirmreversetrack.title"), + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION && (_reversePointsConfirmed = true))) + { + UndoReverseSection undo = new UndoReverseSection(selStart, selEnd); + // call track to reverse range + if (_track.reverseRange(selStart, selEnd)) + { + _undoStack.add(undo); + } + } + } + + + /** + * Interpolate the two selected points + */ + public void interpolateSelection() + { + // Get number of points to add + Object numPointsStr = JOptionPane.showInputDialog(_frame, + I18nManager.getText("dialog.interpolate.parameter.text"), + I18nManager.getText("dialog.interpolate.title"), + JOptionPane.QUESTION_MESSAGE, null, null, ""); + int numPoints = parseNumber(numPointsStr); + if (numPoints <= 0) return; + + UndoInsert undo = new UndoInsert(_trackInfo.getSelection().getStart() + 1, + numPoints); + // call track to interpolate + if (_trackInfo.interpolate(numPoints)) + { + _undoStack.add(undo); + } + } + + + /** + * Rearrange the waypoints into track order + */ + public void rearrangeWaypoints(int inFunction) + { + UndoRearrangeWaypoints undo = new UndoRearrangeWaypoints(_track); + boolean success = false; + if (inFunction == REARRANGE_TO_START || inFunction == REARRANGE_TO_END) + { + // Collect the waypoints to the start or end of the track + success = _track.collectWaypoints(inFunction == REARRANGE_TO_START); + } + else + { + // Interleave the waypoints into track order + success = _track.interleaveWaypoints(); + } + if (success) + { + _undoStack.add(undo); + } + else + { + JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.rearrange.noop"), + I18nManager.getText("error.function.noop.title"), JOptionPane.WARNING_MESSAGE); + } + } + + + /** + * Open a new window with the 3d view + */ + public void show3dWindow() + { + // TODO: open 3d view window + JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.function.notimplemented"), + I18nManager.getText("error.function.notimplemented.title"), JOptionPane.WARNING_MESSAGE); + } + + + /** + * Select all points + */ + public void selectAll() + { + _trackInfo.getSelection().select(0, 0, _track.getNumPoints()-1); + } + + /** + * Select nothing + */ + public void selectNone() + { + _trackInfo.getSelection().clearAll(); + } + + /** + * Receive loaded data and optionally merge with current Track + * @param inFieldArray array of fields + * @param inDataArray array of data + */ + public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename) + { + // Decide whether to load or append + if (_track != null && _track.getNumPoints() > 0) + { + // ask whether to replace or append + int answer = JOptionPane.showConfirmDialog(_frame, + I18nManager.getText("dialog.openappend.text"), + I18nManager.getText("dialog.openappend.title"), + JOptionPane.YES_NO_CANCEL_OPTION); + if (answer == JOptionPane.YES_OPTION) + { + // append data to current Track + Track loadedTrack = new Track(_broker); + loadedTrack.load(inFieldArray, inDataArray, inAltFormat); + _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints())); + _track.combine(loadedTrack); + _trackInfo.getFileInfo().addFile(); + } + else if (answer == JOptionPane.NO_OPTION) + { + // Don't append, replace data + _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length)); + _lastSavePosition = _undoStack.size(); + _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat); + _trackInfo.getFileInfo().setFile(inFilename); + } + } + else + { + // currently no data held, so use received data + _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length)); + _lastSavePosition = _undoStack.size(); + _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat); + _trackInfo.getFileInfo().setFile(inFilename); + } + _broker.informSubscribers(); + // update menu + _menuManager.informFileLoaded(); + } + + + /** + * Inform the app that the data has been saved + */ + public void informDataSaved() + { + _lastSavePosition = _undoStack.size(); + } + + + /** + * Begin undo process + */ + public void beginUndo() + { + if (_undoStack.isEmpty()) + { + JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.undo.none.text"), + I18nManager.getText("dialog.undo.none.title"), JOptionPane.INFORMATION_MESSAGE); + } + else + { + new UndoManager(this, _frame); + } + } + + + /** + * Clear the undo stack (losing all undo information + */ + public void clearUndo() + { + // Exit if nothing to undo + if (_undoStack == null || _undoStack.isEmpty()) + return; + // Has track got unsaved data? + boolean unsaved = hasDataUnsaved(); + // Confirm operation with dialog + int answer = JOptionPane.showConfirmDialog(_frame, + I18nManager.getText("dialog.clearundo.text"), + I18nManager.getText("dialog.clearundo.title"), + JOptionPane.YES_NO_OPTION); + if (answer == JOptionPane.YES_OPTION) + { + _undoStack.clear(); + _lastSavePosition = 0; + if (unsaved) _lastSavePosition = -1; + _broker.informSubscribers(); + } + } + + + /** + * Undo the specified number of actions + * @param inNumUndos number of actions to undo + */ + public void undoActions(int inNumUndos) + { + try + { + for (int i=0; i= 0. + */ +public class AltitudeRange +{ + private IntegerRange _range = new IntegerRange(); + private int _format = Altitude.FORMAT_NONE; + + + /** + * Add a value to the range + * @param inValue value to add, only positive values considered + */ + public void addValue(Altitude inAltitude) + { + if (inAltitude != null) + { + int altValue = inAltitude.getValue(_format); + _range.addValue(altValue); + if (_format == Altitude.FORMAT_NONE) + { + _format = inAltitude.getFormat(); + } + } + } + + + /** + * @return true if positive data values were found + */ + public boolean hasData() + { + return (_range.hasData()); + } + + + /** + * @return minimum value, or -1 if none found + */ + public int getMinimum() + { + return _range.getMinimum(); + } + + + /** + * @return maximum value, or -1 if none found + */ + public int getMaximum() + { + return _range.getMaximum(); + } + + + /** + * @return the altitude format used + */ + public int getFormat() + { + return _format; + } +} diff --git a/tim/prune/data/Coordinate.java b/tim/prune/data/Coordinate.java new file mode 100644 index 0000000..e297a8d --- /dev/null +++ b/tim/prune/data/Coordinate.java @@ -0,0 +1,279 @@ +package tim.prune.data; + +/** + * Class to represent a lat/long coordinate + * and provide conversion functions + */ +public abstract class Coordinate +{ + public static final int NORTH = 0; + public static final int EAST = 1; + public static final int SOUTH = 2; + public static final int WEST = 3; + public static final char[] PRINTABLE_CARDINALS = {'N', 'E', 'S', 'W'}; + public static final int FORMAT_DEG_MIN_SEC = 10; + public static final int FORMAT_DEG_MIN = 11; + public static final int FORMAT_DEG = 12; + public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13; + public static final int FORMAT_NONE = 19; + + // Instance variables + private boolean _valid = false; + protected int _cardinal = NORTH; + private int _degrees = 0; + private int _minutes = 0; + private int _seconds = 0; + private int _fracs = 0; + private String _originalString = null; + private int _originalFormat = FORMAT_NONE; + private double _asDouble = 0.0; + + + /** + * Constructor given String + * @param inString string to parse + */ + public Coordinate(String inString) + { + _originalString = inString; + int strLen = 0; + if (inString != null) + { + inString = inString.trim(); + strLen = inString.length(); + } + if (strLen > 1) + { + // Check for leading character NSEW + _cardinal = getCardinal(inString.charAt(0)); + // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s + int numFields = 0; + boolean inNumeric = false; + char currChar; + long[] fields = new long[4]; + long[] denoms = new long[4]; + try + { + for (int i=0; i= '0' && currChar <= '9') + { + if (!inNumeric) + { + inNumeric = true; + numFields++; + denoms[numFields-1] = 1; + } + fields[numFields-1] = fields[numFields-1] * 10 + (currChar - '0'); + denoms[numFields-1] *= 10; + } + else + { + inNumeric = false; + } + } + _valid = (numFields > 0); + } + catch (ArrayIndexOutOfBoundsException obe) + { + // more than four fields found - unable to parse + _valid = false; + } + // parse fields according to number found + _degrees = (int) fields[0]; + _originalFormat = FORMAT_DEG; + if (numFields == 2) + { + // String is just decimal degrees + double numMins = fields[1] * 60.0 / denoms[1]; + _minutes = (int) numMins; + double numSecs = (numMins - _minutes) * 60.0; + _seconds = (int) numSecs; + _fracs = (int) ((numSecs - _seconds) * 10); + } + else if (numFields == 3) + { + // String is degrees-minutes.fractions + _originalFormat = FORMAT_DEG_MIN; + _minutes = (int) fields[1]; + double numSecs = fields[2] * 60.0 / denoms[2]; + _seconds = (int) numSecs; + _fracs = (int) ((numSecs - _seconds) * 10); + } + else if (numFields == 4) + { + _originalFormat = FORMAT_DEG_MIN_SEC; + // String is degrees-minutes-seconds.fractions + _minutes = (int) fields[1]; + _seconds = (int) fields[2]; + _fracs = (int) fields[3]; + } + _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 36000.0); + if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-') + _asDouble = -_asDouble; + } + else _valid = false; + } + + + /** + * Get the cardinal from the given character + * @param inChar character from file + */ + protected abstract int getCardinal(char inChar); + + + /** + * Constructor + * @param inValue value of coordinate + * @param inFormat format to use + */ + protected Coordinate(double inValue, int inFormat) + { + _asDouble = inValue; + // Calculate degrees, minutes, seconds + _degrees = (int) inValue; + double numMins = (Math.abs(_asDouble)-Math.abs(_degrees)) * 60.0; + _minutes = (int) numMins; + double numSecs = (numMins - _minutes) * 60.0; + _seconds = (int) numSecs; + _fracs = (int) ((numSecs - _seconds) * 10); + // Make a string to display on screen + _originalFormat = FORMAT_NONE; + if (inFormat == FORMAT_NONE) inFormat = FORMAT_DEG_WITHOUT_CARDINAL; + _originalString = output(inFormat); + _originalFormat = inFormat; + } + + + /** + * @return coordinate as a double + */ + public double getDouble() + { + return _asDouble; + } + + /** + * @return true if Coordinate is valid + */ + public boolean isValid() + { + return _valid; + } + + /** + * Compares two Coordinates for equality + * @param inOther other Coordinate object with which to compare + * @return true if the two objects are equal + */ + public boolean equals(Coordinate inOther) + { + return (inOther != null && _cardinal == inOther._cardinal + && _degrees == inOther._degrees + && _minutes == inOther._minutes + && _seconds == inOther._seconds + && _fracs == inOther._fracs); + } + + + /** + * Output the Coordinate in the given format + * @param inOriginalString the original String to use as default + * @param inFormat format to use, eg FORMAT_DEG_MIN_SEC + * @return String for output + */ + public String output(int inFormat) + { + String answer = _originalString; + if (inFormat != FORMAT_NONE && inFormat != _originalFormat) + { + // TODO: allow specification of precision for output of d-m and d + // format as specified + switch (inFormat) + { + case FORMAT_DEG_MIN_SEC: { + StringBuffer buffer = new StringBuffer(); + buffer.append(PRINTABLE_CARDINALS[_cardinal]) + .append(threeDigitString(_degrees)).append('°') + .append(twoDigitString(_minutes)).append('\'') + .append(twoDigitString(_seconds)).append('.') + .append(_fracs); + answer = buffer.toString(); break; + } + case FORMAT_DEG_MIN: answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°" + + (_minutes + _seconds / 60.0 + _fracs / 600.0); break; + case FORMAT_DEG: + case FORMAT_DEG_WITHOUT_CARDINAL: answer = (_asDouble<0.0?"-":"") + + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 36000.0); break; + } + } + return answer; + } + + + /** + * Format an integer to a two-digit String + * @param inNumber number to format + * @return two-character String + */ + private static String twoDigitString(int inNumber) + { + if (inNumber <= 0) return "00"; + if (inNumber < 10) return "0" + inNumber; + if (inNumber < 100) return "" + inNumber; + return "" + (inNumber % 100); + } + + + /** + * Format an integer to a three-digit String for degrees + * @param inNumber number to format + * @return three-character String + */ + private static String threeDigitString(int inNumber) + { + if (inNumber <= 0) return "000"; + if (inNumber < 10) return "00" + inNumber; + if (inNumber < 100) return "0" + inNumber; + return "" + (inNumber % 1000); + } + + + /** + * Create a new Coordinate between two others + * @param inStart start coordinate + * @param inEnd end coordinate + * @param inIndex index of point + * @param inNumPoints number of points to interpolate + * @return new Coordinate object + */ + public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd, + int inIndex, int inNumPoints) + { + double startValue = inStart.getDouble(); + double endValue = inEnd.getDouble(); + double newValue = startValue + (endValue - startValue) * (inIndex+1) / (inNumPoints + 1); + Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat); + return answer; + } + + + /** + * Make a new Coordinate according to subclass + * @param inValue double value + * @param inFormat format to use + * @return object of Coordinate subclass + */ + protected abstract Coordinate makeNew(double inValue, int inFormat); + + + /** + * Create a String representation for debug + */ + public String toString() + { + return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "." + _fracs + ") = " + _asDouble; + } +} diff --git a/tim/prune/data/DataPoint.java b/tim/prune/data/DataPoint.java new file mode 100644 index 0000000..5d66ec5 --- /dev/null +++ b/tim/prune/data/DataPoint.java @@ -0,0 +1,213 @@ +package tim.prune.data; + +/** + * Class to represent a single data point in the series + * including all its fields + * Can be either a track point or a waypoint + */ +public class DataPoint +{ + // Hold these as Strings? Or FieldValue objects? + private String[] _fieldValues = null; + // list of fields + private FieldList _fieldList = null; + // Special fields for coordinates + private Coordinate _latitude = null, _longitude = null; + private Altitude _altitude; + private Timestamp _timestamp = null; + private boolean _pointValid = false; + + + // TODO: Make it possible to turn track point into waypoint - may need to alter FieldList + + /** + * Constructor + * @param inValueArray array of String values + * @param inFieldList list of fields + * @param inAltFormat altitude format + */ + public DataPoint(String[] inValueArray, FieldList inFieldList, int inAltFormat) + { + // save data + _fieldValues = inValueArray; + // save list of fields + _fieldList = inFieldList; + + // parse fields + _latitude = new Latitude(getFieldValue(Field.LATITUDE)); + _longitude = new Longitude(getFieldValue(Field.LONGITUDE)); + _altitude = new Altitude(getFieldValue(Field.ALTITUDE), inAltFormat); + _timestamp = new Timestamp(getFieldValue(Field.TIMESTAMP)); + } + + + /** + * Private constructor for artificially generated points (eg interpolated) + * @param inLatitude latitude + * @param inLongitude longitude + * @param inAltitude altitude + */ + private DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude) + { + // Only these three fields are available + _fieldValues = new String[0]; + _fieldList = new FieldList(); + _latitude = inLatitude; + _longitude = inLongitude; + _altitude = inAltitude; + _timestamp = new Timestamp(null); + } + + + /** + * Get the value for the given field + * @param inField field to interrogate + * @return value of field + */ + public String getFieldValue(Field inField) + { + return getFieldValue(_fieldList.getFieldIndex(inField)); + } + + + /** + * Get the value at the given index + * @param inIndex index number starting at zero + * @return field value, or null if not found + */ + public String getFieldValue(int inIndex) + { + if (_fieldValues == null || inIndex < 0 || inIndex >= _fieldValues.length) + return null; + return _fieldValues[inIndex]; + } + + + public Coordinate getLatitude() + { + return _latitude; + } + public Coordinate getLongitude() + { + return _longitude; + } + public boolean hasAltitude() + { + return _altitude.isValid(); + } + public Altitude getAltitude() + { + return _altitude; + } + public boolean hasTimestamp() + { + return _timestamp.isValid(); + } + public Timestamp getTimestamp() + { + return _timestamp; + } + + /** + * @return true if point has a waypoint name + */ + public boolean isWaypoint() + { + String name = getFieldValue(Field.WAYPT_NAME); + return (name != null && !name.equals("")); + } + + /** + * Compare two DataPoint objects to see if they + * are duplicates + * @param inOther other object to compare + * @return true if the points are equivalent + */ + public boolean isDuplicate(DataPoint inOther) + { + if (inOther == null) return false; + if (_longitude == null || _latitude == null + || inOther._longitude == null || inOther._latitude == null) + { + return false; + } + // Compare latitude and longitude + if (!_longitude.equals(inOther._longitude) || !_latitude.equals(inOther._latitude)) + { + return false; + } + // Note that conversion from decimal to dms can make non-identical points into duplicates + // Compare description (if any) + String name1 = getFieldValue(Field.WAYPT_NAME); + String name2 = inOther.getFieldValue(Field.WAYPT_NAME); + if (name1 == null || name1.equals("")) + { + return (name2 == null || name2.equals("")); + } + else + { + return (name2 != null && name2.equals(name1)); + } + } + + + /** + * @return true if the point is valid + */ + public boolean isValid() + { + return _latitude.isValid() && _longitude.isValid(); + } + + + /** + * Interpolate a set of points between this one and the given one + * @param inEndPoint end point of interpolation + * @param inNumPoints number of points to generate + * @return the DataPoint array + */ + public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints) + { + DataPoint[] range = new DataPoint[inNumPoints]; + Coordinate endLatitude = inEndPoint.getLatitude(); + Coordinate endLongitude = inEndPoint.getLongitude(); + Altitude endAltitude = inEndPoint.getAltitude(); + + // Loop over points + for (int i=0; i _max || _empty) _max = inValue; + _empty = false; + } + + + /** + * @return true if data values were found + */ + public boolean hasData() + { + return (!_empty); + } + + + /** + * @return minimum value, or 0.0 if none found + */ + public double getMinimum() + { + return _min; + } + + + /** + * @return maximum value, or 0.0 if none found + */ + public double getMaximum() + { + return _max; + } +} diff --git a/tim/prune/data/Field.java b/tim/prune/data/Field.java new file mode 100644 index 0000000..118fae3 --- /dev/null +++ b/tim/prune/data/Field.java @@ -0,0 +1,90 @@ +package tim.prune.data; + +import tim.prune.I18nManager; + +/** + * Class to represent a field of a data point + * including its type + */ +public class Field +{ + private String _labelKey = null; + private String _customLabel = null; + private FieldType _type = null; + private boolean _builtin = false; + + public static final Field LATITUDE = new Field("fieldname.latitude", FieldType.COORD); + public static final Field LONGITUDE = new Field("fieldname.longitude", FieldType.COORD); + public static final Field ALTITUDE = new Field("fieldname.altitude", FieldType.INT); + public static final Field TIMESTAMP = new Field("fieldname.timestamp", FieldType.TIME); + public static final Field WAYPT_NAME = new Field("fieldname.waypointname", FieldType.NONE); + public static final Field WAYPT_TYPE = new Field("fieldname.waypointtype", FieldType.NONE); + public static final Field NEW_SEGMENT = new Field("fieldname.newsegment", FieldType.BOOL); + + public static final Field[] ALL_AVAILABLE_FIELDS = { + LATITUDE, LONGITUDE, ALTITUDE, TIMESTAMP, WAYPT_NAME, WAYPT_TYPE, NEW_SEGMENT, + new Field("fieldname.custom", FieldType.NONE) + }; + + /** + * Private constructor + * @param inLabelKey Key for label texts + * @param inType type of field + */ + private Field(String inLabelKey, FieldType inType) + { + _labelKey = inLabelKey; + _customLabel = null; + _type = inType; + _builtin = true; + } + + + /** + * Public constructor for custom fields + * @param inLabel label to use for display + */ + public Field(String inLabel) + { + _labelKey = null; + _customLabel = inLabel; + _type = FieldType.NONE; + } + + /** + * @return the name of the field + */ + public String getName() + { + if (_labelKey != null) + return I18nManager.getText(_labelKey); + return _customLabel; + } + + /** + * Change the name of the (non built-in) field + * @param inName new name + */ + public void setName(String inName) + { + if (!isBuiltIn()) _customLabel = inName; + } + + /** + * @return true if this is a built-in field + */ + public boolean isBuiltIn() + { + return _builtin; + } + + /** + * Checks if the two fields are equal + * @param inOther other Field object + * @return true if Fields identical + */ + public boolean equals(Field inOther) + { + return (isBuiltIn() == inOther.isBuiltIn() && getName().equals(inOther.getName())); + } +} diff --git a/tim/prune/data/FieldList.java b/tim/prune/data/FieldList.java new file mode 100644 index 0000000..58cf6d5 --- /dev/null +++ b/tim/prune/data/FieldList.java @@ -0,0 +1,152 @@ +package tim.prune.data; + +/** + * Class to hold an ordered list of fields + * to match the value list in a data point + */ +public class FieldList +{ + private Field[] _fieldArray; + + + /** + * Constructor for an empty field list + */ + public FieldList() + { + _fieldArray = new Field[0]; + } + + /** + * Constructor for a given number of empty fields + * @param inNumFields + */ + public FieldList(int inNumFields) + { + if (inNumFields < 0) inNumFields = 0; + _fieldArray = new Field[inNumFields]; + } + + /** + * Constructor giving array of Field objects + * @param inFieldArray + */ + public FieldList(Field[] inFieldArray) + { + if (inFieldArray == null || inFieldArray.length == 0) + { + _fieldArray = new Field[0]; + } + else + { + _fieldArray = new Field[inFieldArray.length]; + System.arraycopy(inFieldArray, 0, _fieldArray, 0, inFieldArray.length); + } + } + + /** + * Get the index of the given field + * @param inField field to look for + * @return index number of the field starting at zero + */ + public int getFieldIndex(Field inField) + { + if (inField == null) return -1; + for (int f=0; f<_fieldArray.length; f++) + { + if (_fieldArray[f] != null && _fieldArray[f].equals(inField)) + return f; + } + return -1; + } + + + /** + * Check whether the FieldList contains the given + * Field object + * @param inField Field to check + * @return true if the FieldList contains the given field + */ + public boolean contains(Field inField) + { + return (getFieldIndex(inField) >= 0); + } + + + /** + * @return number of fields in list + */ + public int getNumFields() + { + if (_fieldArray == null) return 0; + return _fieldArray.length; + } + + + /** + * Get the specified Field object + * @param inIndex index to retrieve + * @return Field object + */ + public Field getField(int inIndex) + { + if (_fieldArray == null || inIndex < 0 || inIndex >= _fieldArray.length) + { + return null; + } + return _fieldArray[inIndex]; + } + + + /** + * Merge this list with a second list, giving a superset + * @param inOtherList other FieldList object to merge + * @return Merged FieldList object + */ + public FieldList merge(FieldList inOtherList) + { + // count number of fields + int totalFields = _fieldArray.length; + for (int f=0; f _fieldArray.length) + { + int fieldCounter = _fieldArray.length; + for (int f=0; f= 0. + */ +public class IntegerRange +{ + private int _min = -1, _max = -1; + + + /** + * Add a value to the range + * @param inValue value to add, only positive values considered + */ + public void addValue(int inValue) + { + if (inValue >= 0) + { + if (inValue < _min || _min < 0) _min = inValue; + if (inValue > _max) _max = inValue; + } + } + + + /** + * @return true if positive data values were found + */ + public boolean hasData() + { + return (_max >= 0); + } + + + /** + * @return minimum value, or -1 if none found + */ + public int getMinimum() + { + return _min; + } + + + /** + * @return maximum value, or -1 if none found + */ + public int getMaximum() + { + return _max; + } +} diff --git a/tim/prune/data/Latitude.java b/tim/prune/data/Latitude.java new file mode 100644 index 0000000..6d8bab6 --- /dev/null +++ b/tim/prune/data/Latitude.java @@ -0,0 +1,64 @@ +package tim.prune.data; + +/** + * Class to represent a Latitude Coordinate + */ +public class Latitude extends Coordinate +{ + /** + * Constructor + * @param inString string value from file + */ + public Latitude(String inString) + { + super(inString); + } + + + /** + * Constructor + * @param inValue value of coordinate + * @param inFormat format to use + */ + protected Latitude(double inValue, int inFormat) + { + super(inValue, inFormat); + _cardinal = inValue < 0.0 ? SOUTH : NORTH; + } + + + /** + * Turn the given character into a cardinal + * @see tim.prune.data.Coordinate#getCardinal(char) + */ + protected int getCardinal(char inChar) + { + // Latitude recognises N, S and - + // default is North + int cardinal = NORTH; + switch (inChar) + { + case 'N': + case 'n': + cardinal = NORTH; break; + case 'S': + case 's': + case '-': + cardinal = SOUTH; break; + default: + // no character given + } + return cardinal; + } + + + /** + * Make a new Latitude object + * @see tim.prune.data.Coordinate#makeNew(double, int) + */ + protected Coordinate makeNew(double inValue, int inFormat) + { + return new Latitude(inValue, inFormat); + } + +} diff --git a/tim/prune/data/Longitude.java b/tim/prune/data/Longitude.java new file mode 100644 index 0000000..d61adbc --- /dev/null +++ b/tim/prune/data/Longitude.java @@ -0,0 +1,64 @@ +package tim.prune.data; + +/** + * Class to represent a Longitude Coordinate + */ +public class Longitude extends Coordinate +{ + /** + * Constructor + * @param inString string value from file + */ + public Longitude(String inString) + { + super(inString); + } + + + /** + * Constructor + * @param inValue value of coordinate + * @param inFormat format to use + */ + protected Longitude(double inValue, int inFormat) + { + super(inValue, inFormat); + _cardinal = inValue < 0.0 ? WEST : EAST; + } + + + /** + * Turn the given character into a cardinal + * @see tim.prune.data.Coordinate#getCardinal(char) + */ + protected int getCardinal(char inChar) + { + // Longitude recognises E, W and - + // default is East + int cardinal = EAST; + switch (inChar) + { + case 'E': + case 'e': + cardinal = EAST; break; + case 'W': + case 'w': + case '-': + cardinal = WEST; break; + default: + // no character given + } + return cardinal; + } + + + /** + * Make a new Longitude object + * @see tim.prune.data.Coordinate#makeNew(double, int) + */ + protected Coordinate makeNew(double inValue, int inFormat) + { + return new Longitude(inValue, inFormat); + } + +} diff --git a/tim/prune/data/Selection.java b/tim/prune/data/Selection.java new file mode 100644 index 0000000..c375e82 --- /dev/null +++ b/tim/prune/data/Selection.java @@ -0,0 +1,404 @@ +package tim.prune.data; + +import tim.prune.UpdateMessageBroker; + +/** + * Class to represent a selected portion of a Track + * and its properties + */ +public class Selection +{ + private Track _track = null; + private UpdateMessageBroker _broker = null; + private int _currentPoint = -1; + private boolean _valid = false; + private int _startIndex = -1, _endIndex = -1; + private IntegerRange _altitudeRange = null; + private int _climb = -1, _descent = -1; + private int _altitudeFormat = Altitude.FORMAT_NONE; + private long _seconds = 0L; + private double _angDistance = -1.0; //, _averageSpeed = -1.0; + + + /** + * Constructor + * @param inTrack track object + * @param inBroker broker object + */ + public Selection(Track inTrack, UpdateMessageBroker inBroker) + { + _track = inTrack; + _broker = inBroker; + } + + + /** + * Reset selection to be recalculated + */ + private void reset() + { + _valid = false; + } + + + /** + * Select the point at the given index + * @param inIndex index number of selected point + */ + public void selectPoint(int inIndex) + { + if (inIndex >= -1) + { + _currentPoint = inIndex; + check(); + } + } + + /** + * Select the specified point and range in one go + * @param inPointIndex point selection + * @param inStart range start + * @param inEnd range end + */ + public void select(int inPointIndex, int inStart, int inEnd) + { + _currentPoint = inPointIndex; + _startIndex = inStart; + _endIndex = inEnd; + reset(); + check(); + } + + + /** + * Select the previous point + */ + public void selectPreviousPoint() + { + if (_currentPoint > 0) + selectPoint(_currentPoint - 1); + } + + /** + * Select the next point + */ + public void selectNextPoint() + { + selectPoint(_currentPoint + 1); + } + + /** + * @return the current point index + */ + public int getCurrentPointIndex() + { + return _currentPoint; + } + + + /** + * @return true if range is selected + */ + public boolean hasRangeSelected() + { + return _startIndex >= 0 && _endIndex > _startIndex; + } + + + /** + * Recalculate all selection details + */ + private void recalculate() + { + _altitudeFormat = Altitude.FORMAT_NONE; + if (_track.getNumPoints() > 0 && hasRangeSelected()) + { + _altitudeRange = new IntegerRange(); + _climb = 0; + _descent = 0; + Altitude altitude = null; + Timestamp time = null, startTime = null, endTime = null; + DataPoint lastPoint = null, currPoint = null; + _angDistance = 0.0; + int altValue = 0; + int lastAltValue = 0; + boolean foundAlt = false; + for (int i=_startIndex; i<=_endIndex; i++) + { + currPoint = _track.getPoint(i); + altitude = currPoint.getAltitude(); + // Ignore waypoints in altitude calculations + if (!currPoint.isWaypoint() && altitude.isValid()) + { + altValue = altitude.getValue(_altitudeFormat); + if (_altitudeFormat == Altitude.FORMAT_NONE) + _altitudeFormat = altitude.getFormat(); + _altitudeRange.addValue(altValue); + if (foundAlt) + { + if (altValue > lastAltValue) + _climb += (altValue - lastAltValue); + else + _descent += (lastAltValue - altValue); + } + lastAltValue = altValue; + foundAlt = true; + } + // Store the first and last timestamp in the range + time = currPoint.getTimestamp(); + if (time.isValid()) + { + if (startTime == null) startTime = time; + endTime = time; + } + // Calculate distances, again ignoring waypoints + if (!currPoint.isWaypoint()) + { + if (lastPoint != null) + { + _angDistance += DataPoint.calculateRadiansBetween(lastPoint, currPoint); + } + lastPoint = currPoint; + } + } + if (endTime != null) + { + _seconds = endTime.getSecondsSince(startTime); + } + else + { + _seconds = 0L; + } + } + _valid = true; + } + + + /** + * @return start index + */ + public int getStart() + { + if (!_valid) recalculate(); + return _startIndex; + } + + + /** + * @return end index + */ + public int getEnd() + { + if (!_valid) recalculate(); + return _endIndex; + } + + + /** + * @return the altitude format, ie feet or metres + */ + public int getAltitudeFormat() + { + return _altitudeFormat; + } + + /** + * @return altitude range + */ + public IntegerRange getAltitudeRange() + { + if (!_valid) recalculate(); + return _altitudeRange; + } + + + /** + * @return climb + */ + public int getClimb() + { + if (!_valid) recalculate(); + return _climb; + } + + /** + * @return descent + */ + public int getDescent() + { + if (!_valid) recalculate(); + return _descent; + } + + + /** + * @return number of seconds spanned by selection + */ + public long getNumSeconds() + { + if (!_valid) recalculate(); + return _seconds; + } + + + /** + * @param inFormat distance units to use, from class Distance + * @return distance of Selection in specified units + */ + public double getDistance(int inUnits) + { + return Distance.convertRadians(_angDistance, inUnits); + } + + + /** + * Clear selected point and range + */ + public void clearAll() + { + _currentPoint = -1; + deselectRange(); + } + + + /** + * Deselect range + */ + public void deselectRange() + { + _startIndex = _endIndex = -1; + reset(); + check(); + } + + + /** + * Select the range from the current point + */ + public void selectRangeStart() + { + selectRangeStart(_currentPoint); + } + + + /** + * Set the index for the start of the range selection + * @param inStartIndex start index + */ + public void selectRangeStart(int inStartIndex) + { + if (inStartIndex < 0) + { + _startIndex = _endIndex = -1; + } + else + { + _startIndex = inStartIndex; + // Move end of selection to max if necessary + if (_endIndex <= _startIndex) + { + _endIndex = _track.getNumPoints() - 1; + } + } + reset(); + _broker.informSubscribers(); + } + + + /** + * Select the range up to the current point + */ + public void selectRangeEnd() + { + selectRangeEnd(_currentPoint); + } + + + /** + * Set the index for the end of the range selection + * @param inEndIndex end index + */ + public void selectRangeEnd(int inEndIndex) + { + if (inEndIndex < 0) + { + _startIndex = _endIndex = -1; + } + else + { + _endIndex = inEndIndex; + // Move start of selection to min if necessary + if (_startIndex > _endIndex || _startIndex < 0) + { + _startIndex = 0; + } + } + reset(); + _broker.informSubscribers(); + } + + + /** + * Modify the selection given that the selected range has been deleted + */ + public void modifyRangeDeleted() + { + // Modify current point, if any + if (_currentPoint > _endIndex) + { + _currentPoint -= (_endIndex - _startIndex); + } + else if (_currentPoint > _startIndex) + { + _currentPoint = _startIndex; + } + // Clear selected range + _startIndex = _endIndex = -1; + // Check for consistency and fire update + check(); + } + + + /** + * Modify the selection when a point is deleted + */ + public void modifyPointDeleted() + { + // current point index doesn't change, just gets checked + // range needs to get altered if deleted point is inside or before + if (hasRangeSelected() && _currentPoint <= _endIndex) + { + _endIndex--; + if (_currentPoint < _startIndex) + _startIndex--; + reset(); + } + check(); + } + + + /** + * Check that the selection still makes sense + * and fire update message to listeners + */ + private void check() + { + if (_track != null && _track.getNumPoints() > 0) + { + int maxIndex = _track.getNumPoints() - 1; + if (_currentPoint > maxIndex) + { + _currentPoint = maxIndex; + } + if (_endIndex > maxIndex) + { + _endIndex = maxIndex; + } + if (_startIndex > maxIndex) + { + _startIndex = maxIndex; + } + } + _broker.informSubscribers(); + } +} diff --git a/tim/prune/data/Timestamp.java b/tim/prune/data/Timestamp.java new file mode 100644 index 0000000..965853a --- /dev/null +++ b/tim/prune/data/Timestamp.java @@ -0,0 +1,150 @@ +package tim.prune.data; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * Class to hold the timestamp of a track point + * and provide conversion functions + */ +public class Timestamp +{ + private boolean _valid = false; + private long _seconds = 0L; + private String _text = null; + + private static DateFormat DEFAULT_DATE_FORMAT = DateFormat.getDateTimeInstance(); + private static DateFormat[] ALL_DATE_FORMATS = null; + private static Calendar CALENDAR = null; + private static long SECS_SINCE_1970 = 0L; + private static long SECS_SINCE_GARTRIP = 0L; + private static long MSECS_SINCE_1970 = 0L; + private static long MSECS_SINCE_1990 = 0L; + private static long TWENTY_YEARS_IN_SECS = 0L; + + private static final long GARTRIP_OFFSET = 631065600L; + + // Static block to initialise offsets + static + { + CALENDAR = Calendar.getInstance(); + MSECS_SINCE_1970 = CALENDAR.getTimeInMillis(); + SECS_SINCE_1970 = MSECS_SINCE_1970 / 1000L; + SECS_SINCE_GARTRIP = SECS_SINCE_1970 - GARTRIP_OFFSET; + CALENDAR.add(Calendar.YEAR, -20); + MSECS_SINCE_1990 = CALENDAR.getTimeInMillis(); + TWENTY_YEARS_IN_SECS = (MSECS_SINCE_1970 - MSECS_SINCE_1990) / 1000L; + // Date formats + ALL_DATE_FORMATS = new DateFormat[] { + DEFAULT_DATE_FORMAT, + new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy"), + new SimpleDateFormat("HH:mm:ss dd MMM yyyy"), + new SimpleDateFormat("dd MMM yyyy HH:mm:ss"), + new SimpleDateFormat("yyyy MMM dd HH:mm:ss") + }; + } + + + /** + * Constructor + */ + public Timestamp(String inString) + { + if (inString != null && !inString.equals("")) + { + // Try to parse into a long + try + { + long rawValue = Long.parseLong(inString.trim()); + // check for each format possibility and pick nearest + long diff1 = Math.abs(SECS_SINCE_1970 - rawValue); + long diff2 = Math.abs(MSECS_SINCE_1970 - rawValue); + long diff3 = Math.abs(MSECS_SINCE_1990 - rawValue); + long diff4 = Math.abs(SECS_SINCE_GARTRIP - rawValue); + + // Start off with "seconds since 1970" format + long smallestDiff = diff1; + _seconds = rawValue; + // Now check millis since 1970 + if (diff2 < smallestDiff) + { + // milliseconds since 1970 + _seconds = rawValue / 1000L; + smallestDiff = diff2; + } + // Now millis since 1990 + if (diff3 < smallestDiff) + { + // milliseconds since 1990 + _seconds = rawValue / 1000L + TWENTY_YEARS_IN_SECS; + smallestDiff = diff3; + } + // Lastly, check garmin offset + if (diff4 < smallestDiff) + { + // milliseconds since garmin offset + _seconds = rawValue + GARTRIP_OFFSET; + } + _valid = true; + } + catch (NumberFormatException nfe) + { + // String is not a long, so try a date/time string instead + // try each of the date formatters in turn + Date date = null; + for (int i=0; i= 0 && inNewSize < getNumPoints()) + { + _numPoints = inNewSize; + // needs to be scaled again + _scaled = false; + _broker.informSubscribers(); + } + } + + + /** + * Compress the track to the given resolution + * @param inResolution resolution + * @return number of points deleted + */ + public int compress(int inResolution) + { + // (maybe should be separate thread?) + // (maybe should be in separate class?) + // (maybe should be based on subtended angles instead of distances?) + + if (inResolution <= 0) return 0; + int numCopied = 0; + // Establish range of track and minimum range between points + scalePoints(); + double wholeScale = _xRange.getMaximum() - _xRange.getMinimum(); + double yscale = _yRange.getMaximum() - _yRange.getMinimum(); + if (yscale > wholeScale) wholeScale = yscale; + double minDist = wholeScale / inResolution; + + // Copy selected points + DataPoint[] newPointArray = new DataPoint[_numPoints]; + int[] pointIndices = new int[_numPoints]; + for (int i=0; i<_numPoints; i++) + { + boolean keepPoint = true; + if (!_dataPoints[i].isWaypoint()) + { + // go through newPointArray to check for range + for (int j=0; j 0) + { + _dataPoints = new DataPoint[numCopied]; + System.arraycopy(newPointArray, 0, _dataPoints, 0, numCopied); + _numPoints = _dataPoints.length; + _scaled = false; + _broker.informSubscribers(); + } + return numDeleted; + } + + + /** + * Halve the track by deleting alternate points + * @return number of points deleted + */ + public int halve() + { + if (_numPoints < 100) return 0; + int newSize = _numPoints / 2; + int numDeleted = _numPoints - newSize; + DataPoint[] newPointArray = new DataPoint[newSize]; + // Delete alternate points + for (int i=0; i 0) + { + System.arraycopy(_dataPoints, 0, newPointArray, 0, inStart); + } + // Copy points after the deleted one(s) + if (inEnd < (_numPoints - 1)) + { + System.arraycopy(_dataPoints, inEnd + 1, newPointArray, inStart, + _numPoints - inEnd - 1); + } + // Copy points over original array (careful!) + _dataPoints = newPointArray; + _numPoints -= numToDelete; + // needs to be scaled again + _scaled = false; + return true; + } + + + /** + * Delete all the duplicate points in the track + * @return number of points deleted + */ + public int deleteDuplicates() + { + // loop through Track counting duplicates first + boolean[] dupes = new boolean[_numPoints]; + int numDupes = 0; + int i, j; + for (i=1; i<_numPoints; i++) + { + DataPoint p1 = _dataPoints[i]; + // Loop through all points before this one + for (j=0; j 0) + { + // Make new resized array and copy DataPoints over + DataPoint[] newPointArray = new DataPoint[_numPoints - numDupes]; + j = 0; + for (i=0; i<_numPoints; i++) + { + if (!dupes[i]) + { + newPointArray[j] = _dataPoints[i]; + j++; + } + } + // Copy array references + _dataPoints = newPointArray; + _numPoints = _dataPoints.length; + _scaled = false; + _broker.informSubscribers(); + } + return numDupes; + } + + + /** + * Reverse the specified range of points + * @return true if successful, false otherwise + */ + public boolean reverseRange(int inStart, int inEnd) + { + if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) + { + return false; + } + // calculate how many point swaps are required + int numPointsToReverse = (inEnd - inStart + 1) / 2; + DataPoint p = null; + for (int i=0; i 0); + } + else + { + nonWaypoints[numNonWaypoints] = point; + numNonWaypoints++; + nonAfterWay |= (numWaypoints > 0); + } + } + // Exit if the data is already in the specified order + if (numWaypoints == 0 || numNonWaypoints == 0 + || (inAtStart && !wayAfterNon && nonAfterWay) + || (!inAtStart && wayAfterNon && !nonAfterWay)) + { + return false; + } + + // Copy the arrays back into _dataPoints in the specified order + if (inAtStart) + { + System.arraycopy(waypoints, 0, _dataPoints, 0, numWaypoints); + System.arraycopy(nonWaypoints, 0, _dataPoints, numWaypoints, numNonWaypoints); + } + else + { + System.arraycopy(nonWaypoints, 0, _dataPoints, 0, numNonWaypoints); + System.arraycopy(waypoints, 0, _dataPoints, numNonWaypoints, numWaypoints); + } + // needs to be scaled again + _scaled = false; + _broker.informSubscribers(); + return true; + } + + + /** + * Interleave all waypoints by each nearest track point + * @return true if successful, false if no change + */ + public boolean interleaveWaypoints() + { + // Separate waypoints and find nearest track point + int numWaypoints = 0; + DataPoint[] waypoints = new DataPoint[_numPoints]; + int[] pointIndices = new int[_numPoints]; + DataPoint point = null; + int i = 0; + for (i=0; i<_numPoints; i++) + { + point = _dataPoints[i]; + if (point.isWaypoint()) + { + waypoints[numWaypoints] = point; + pointIndices[numWaypoints] = getNearestPointIndex( + _xValues[i], _yValues[i], -1.0, true); + numWaypoints++; + } + } + // Exit if data not mixed + if (numWaypoints == 0 || numWaypoints == _numPoints) + return false; + + // Loop round points copying to correct order + DataPoint[] dataCopy = new DataPoint[_numPoints]; + int copyIndex = 0; + for (i=0; i<_numPoints; i++) + { + point = _dataPoints[i]; + // if it's a track point, copy it + if (!point.isWaypoint()) + { + dataCopy[copyIndex] = point; + copyIndex++; + } + // check for waypoints with this index + for (int j=0; j= _numPoints || inNumPoints <= 0) + return false; + + // get start and end points + DataPoint startPoint = getPoint(inStartIndex); + DataPoint endPoint = getPoint(inStartIndex + 1); + + // Make array of points to insert + DataPoint[] insertedPoints = startPoint.interpolate(endPoint, inNumPoints); + + // Insert points into track + insertRange(insertedPoints, inStartIndex + 1); + return true; + } + + + //////// information methods ///////////// + + + /** + * Get the point at the given index + * @param inPointNum index number, starting at 0 + * @return DataPoint object, or null if out of range + */ + public DataPoint getPoint(int inPointNum) + { + if (inPointNum > -1 && inPointNum < getNumPoints()) + { + return _dataPoints[inPointNum]; + } + return null; + } + + + /** + * @return altitude range of points as AltitudeRange object + */ + public AltitudeRange getAltitudeRange() + { + if (!_scaled) scalePoints(); + return _altitudeRange; + } + /** + * @return the number of (valid) points in the track + */ + public int getNumPoints() + { + return _numPoints; + } + + /** + * @return The range of x values as a DoubleRange object + */ + public DoubleRange getXRange() + { + if (!_scaled) scalePoints(); + return _xRange; + } + + /** + * @return The range of y values as a DoubleRange object + */ + public DoubleRange getYRange() + { + if (!_scaled) scalePoints(); + return _yRange; + } + + /** + * @param inPointNum point index, starting at 0 + * @return scaled x value of specified point + */ + public double getX(int inPointNum) + { + if (!_scaled) scalePoints(); + return _xValues[inPointNum]; + } + + /** + * @param inPointNum point index, starting at 0 + * @return scaled y value of specified point + */ + public double getY(int inPointNum) + { + if (!_scaled) scalePoints(); + return _yValues[inPointNum]; + } + + /** + * @return the master field list + */ + public FieldList getFieldList() + { + return _masterFieldList; + } + + + /** + * Checks if any data exists for the specified field + * @param inField Field to examine + * @return true if data exists for this field + */ + public boolean hasData(Field inField) + { + return hasData(inField, 0, _numPoints-1); + } + + + /** + * Checks if any data exists for the specified field in the specified range + * @param inField Field to examine + * @param inStart start of range to check + * @param inEnd end of range to check (inclusive) + * @return true if data exists for this field + */ + public boolean hasData(Field inField, int inStart, int inEnd) + { + for (int i=inStart; i<=inEnd; i++) + { + if (_dataPoints[i].getFieldValue(inField) != null) + { + return true; + } + } + return false; + } + + + /** + * @return true if track contains waypoints and trackpoints + */ + public boolean hasMixedData() + { + if (!_scaled) scalePoints(); + return _mixedData; + } + + + ///////// Internal processing methods //////////////// + + + /** + * Scale all the points in the track to gain x and y values + * ready for plotting + */ + private void scalePoints() + { + // Loop through all points in track, to see limits of lat, long and altitude + _longRange = new DoubleRange(); + _latRange = new DoubleRange(); + _altitudeRange = new AltitudeRange(); + int p; + boolean hasWaypoint = false, hasTrackpoint = false; + for (p=0; p < getNumPoints(); p++) + { + DataPoint point = getPoint(p); + if (point != null && point.isValid()) + { + _longRange.addValue(point.getLongitude().getDouble()); + _latRange.addValue(point.getLatitude().getDouble()); + if (point.getAltitude().isValid()) + _altitudeRange.addValue(point.getAltitude()); + if (point.isWaypoint()) + hasWaypoint = true; + else + hasTrackpoint = true; + } + } + _mixedData = hasWaypoint && hasTrackpoint; + + // Use medians to centre at 0 + double longMedian = (_longRange.getMaximum() + _longRange.getMinimum()) / 2.0; + double latMedian = (_latRange.getMaximum() + _latRange.getMinimum()) / 2.0; + double longFactor = Math.cos(latMedian / 180.0 * Math.PI); // Function of median latitude + + // Loop over points and calculate scales + _xValues = new double[getNumPoints()]; + _yValues = new double[getNumPoints()]; + _xRange = new DoubleRange(); + _yRange = new DoubleRange(); + for (p=0; p < getNumPoints(); p++) + { + DataPoint point = getPoint(p); + if (point != null) + { + _xValues[p] = (point.getLongitude().getDouble() - longMedian) * longFactor; + _xRange.addValue(_xValues[p]); + _yValues[p] = (point.getLatitude().getDouble() - latMedian); + _yRange.addValue(_yValues[p]); + } + } + _scaled = true; + } + + + /** + * Find the nearest point to the specified x and y coordinates + * or -1 if no point is within the specified max distance + * @param inX x coordinate + * @param inY y coordinate + * @param inMaxDist maximum distance from selected coordinates + * @param inJustTrackPoints true if waypoints should be ignored + * @return index of nearest point or -1 if not found + */ + public int getNearestPointIndex(double inX, double inY, double inMaxDist, boolean inJustTrackPoints) + { + int nearestPoint = 0; + double nearestDist = -1.0; + double currDist; + for (int i=0; i < getNumPoints(); i++) + { + if (!inJustTrackPoints || !_dataPoints[i].isWaypoint()) + { + currDist = Math.abs(_xValues[i] - inX) + Math.abs(_yValues[i] - inY); + if (currDist < nearestDist || nearestDist < 0.0) + { + nearestPoint = i; + nearestDist = currDist; + } + } + } + // Check whether it's within required distance + if (nearestDist > inMaxDist && inMaxDist > 0.0) + { + return -1; + } + return nearestPoint; + } + + + ////////////////// Cloning and replacing /////////////////// + + /** + * Clone the array of DataPoints + * @return shallow copy of DataPoint objects + */ + public DataPoint[] cloneContents() + { + DataPoint[] clone = new DataPoint[getNumPoints()]; + System.arraycopy(_dataPoints, 0, clone, 0, getNumPoints()); + return clone; + } + + + /** + * Clone the specified range of data points + * @param inStart start index (inclusive) + * @param inEnd end index (inclusive) + * @return shallow copy of DataPoint objects + */ + public DataPoint[] cloneRange(int inStart, int inEnd) + { + int numSelected = 0; + if (inEnd >= 0 && inEnd >= inStart) + { + numSelected = inEnd - inStart + 1; + } + DataPoint[] result = new DataPoint[numSelected>0?numSelected:0]; + if (numSelected > 0) + { + System.arraycopy(_dataPoints, inStart, result, 0, numSelected); + } + return result; + } + + + /** + * Re-insert the specified point at the given index + * @param inPoint point to insert + * @param inIndex index at which to insert the point + * @return true if it worked, false otherwise + */ + public boolean insertPoint(DataPoint inPoint, int inIndex) + { + if (inIndex > _numPoints || inPoint == null) + { + return false; + } + // Make new array to copy points over to + DataPoint[] newPointArray = new DataPoint[_numPoints + 1]; + if (inIndex > 0) + { + System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex); + } + newPointArray[inIndex] = inPoint; + if (inIndex < _numPoints) + { + System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+1, _numPoints - inIndex); + } + // Change over to new array + _dataPoints = newPointArray; + _numPoints++; + // needs to be scaled again + _scaled = false; + _broker.informSubscribers(); + return true; + } + + + /** + * Re-insert the specified point range at the given index + * @param inPoints point array to insert + * @param inIndex index at which to insert the points + * @return true if it worked, false otherwise + */ + public boolean insertRange(DataPoint[] inPoints, int inIndex) + { + if (inIndex > _numPoints || inPoints == null) + { + return false; + } + // Make new array to copy points over to + DataPoint[] newPointArray = new DataPoint[_numPoints + inPoints.length]; + if (inIndex > 0) + { + System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex); + } + System.arraycopy(inPoints, 0, newPointArray, inIndex, inPoints.length); + if (inIndex < _numPoints) + { + System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+inPoints.length, _numPoints - inIndex); + } + // Change over to new array + _dataPoints = newPointArray; + _numPoints += inPoints.length; + // needs to be scaled again + _scaled = false; + _broker.informSubscribers(); + return true; + } + + + /** + * Replace the track contents with the given point array + * @param inContents array of DataPoint objects + */ + public boolean replaceContents(DataPoint[] inContents) + { + // master field array stays the same + // (would need to store field array too if we wanted to redo a load) + // replace data array + _dataPoints = inContents; + _numPoints = _dataPoints.length; + _scaled = false; + _broker.informSubscribers(); + return true; + } +} diff --git a/tim/prune/data/TrackInfo.java b/tim/prune/data/TrackInfo.java new file mode 100644 index 0000000..71363da --- /dev/null +++ b/tim/prune/data/TrackInfo.java @@ -0,0 +1,162 @@ +package tim.prune.data; + +import tim.prune.UpdateMessageBroker; + +/** + * Class to hold all track information, including data + * and the selection information + */ +public class TrackInfo +{ + private UpdateMessageBroker _broker = null; + private Track _track = null; + private Selection _selection = null; + private FileInfo _fileInfo = null; + + /** + * Constructor + * @param inTrack Track object + * @param inBroker broker object + */ + public TrackInfo(Track inTrack, UpdateMessageBroker inBroker) + { + _broker = inBroker; + _track = inTrack; + _selection = new Selection(_track, inBroker); + _fileInfo = new FileInfo(); + } + + + /** + * @return the Track object + */ + public Track getTrack() + { + return _track; + } + + + /** + * @return the Selection object + */ + public Selection getSelection() + { + return _selection; + } + + + /** + * @return the FileInfo object + */ + public FileInfo getFileInfo() + { + return _fileInfo; + } + + /** + * Get the currently selected point, if any + * @return DataPoint if single point selected, otherwise null + */ + public DataPoint getCurrentPoint() + { + return _track.getPoint(_selection.getCurrentPointIndex()); + } + + + /** + * Load the specified data into the Track + * @param inFieldArray array of Field objects describing fields + * @param inPointArray 2d object array containing data + * @param inAltFormat altitude format + */ + public void loadTrack(Field[] inFieldArray, Object[][] inPointArray, int inAltFormat) + { + _track.cropTo(0); + _track.load(inFieldArray, inPointArray, inAltFormat); + _selection.clearAll(); + } + + + /** + * Delete the currently selected range of points + * @return true if successful + */ + public boolean deleteRange() + { + int currPoint = _selection.getCurrentPointIndex(); + int startSel = _selection.getStart(); + int endSel = _selection.getEnd(); + boolean answer = _track.deleteRange(startSel, endSel); + // clear range selection + _selection.modifyRangeDeleted(); + return answer; + } + + + /** + * Delete the currently selected point + * @return true if point deleted + */ + public boolean deletePoint() + { + if (_track.deletePoint(_selection.getCurrentPointIndex())) + { + _selection.modifyPointDeleted(); + _broker.informSubscribers(); + return true; + } + return false; + } + + + /** + * Compress the track to the given resolution + * @param inResolution resolution + * @return number of points deleted + */ + public int compress(int inResolution) + { + int numDeleted = _track.compress(inResolution); + if (numDeleted > 0) + _selection.clearAll(); + return numDeleted; + } + + + /** + * Delete all the duplicate points in the track + * @return number of points deleted + */ + public int deleteDuplicates() + { + int numDeleted = _track.deleteDuplicates(); + if (numDeleted > 0) + _selection.clearAll(); + return numDeleted; + } + + + /** + * Clone the selected range of data points + * @return shallow copy of DataPoint objects + */ + public DataPoint[] cloneSelectedRange() + { + return _track.cloneRange(_selection.getStart(), _selection.getEnd()); + } + + + /** + * Interpolate extra points between two selected ones + * @param inStartIndex start index of interpolation + * @param inNumPoints num points to insert + * @return true if successful + */ + public boolean interpolate(int inNumPoints) + { + boolean success = _track.interpolate(_selection.getStart(), inNumPoints); + if (success) + _selection.selectRangeEnd(_selection.getEnd() + inNumPoints); + return success; + } +} diff --git a/tim/prune/gui/AboutScreen.java b/tim/prune/gui/AboutScreen.java new file mode 100644 index 0000000..8e5c1da --- /dev/null +++ b/tim/prune/gui/AboutScreen.java @@ -0,0 +1,90 @@ +package tim.prune.gui; + +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JEditorPane; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import tim.prune.GpsPruner; +import tim.prune.I18nManager; + +/** + * Class to represent the "About" popup window + */ +public class AboutScreen extends JDialog +{ + + /** + * Constructor + */ + public AboutScreen(JFrame inParent) + { + super(inParent, I18nManager.getText("dialog.about.title")); + getContentPane().add(makeContents()); + } + + + /** + * @return the contents of the window as a Component + */ + private Component makeContents() + { + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + JLabel titleLabel = new JLabel("Prune"); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 24)); + titleLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); + mainPanel.add(titleLabel); + JLabel versionLabel = new JLabel(I18nManager.getText("dialog.about.version") + ": " + GpsPruner.VERSION_NUMBER); + versionLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); + mainPanel.add(versionLabel); + JLabel buildLabel = new JLabel(I18nManager.getText("dialog.about.build") + ": " + GpsPruner.BUILD_NUMBER); + buildLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); + mainPanel.add(buildLabel); + mainPanel.add(new JLabel(" ")); + StringBuffer descBuffer = new StringBuffer(); + descBuffer.append("

").append(I18nManager.getText("dialog.about.summarytext1")).append("

"); + descBuffer.append("

").append(I18nManager.getText("dialog.about.summarytext2")).append("

"); + descBuffer.append("

").append(I18nManager.getText("dialog.about.summarytext3")).append("

"); + JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString()); + descPane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + descPane.setEditable(false); + descPane.setOpaque(false); + descPane.setAlignmentX(JEditorPane.CENTER_ALIGNMENT); + // descPane.setBackground(Color.GRAY); + mainPanel.add(descPane); + mainPanel.add(new JLabel(" ")); + JButton okButton = new JButton(I18nManager.getText("button.ok")); + okButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + dispose(); + } + }); + okButton.setAlignmentX(JButton.CENTER_ALIGNMENT); + mainPanel.add(okButton); + return mainPanel; + } + + + /** + * Show window + */ + public void show() + { + pack(); + // setSize(300,200); + super.show(); + } +} diff --git a/tim/prune/gui/DetailsDisplay.java b/tim/prune/gui/DetailsDisplay.java new file mode 100644 index 0000000..e9ed747 --- /dev/null +++ b/tim/prune/gui/DetailsDisplay.java @@ -0,0 +1,449 @@ +package tim.prune.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.text.NumberFormat; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.border.EtchedBorder; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.Coordinate; +import tim.prune.data.DataPoint; +import tim.prune.data.Distance; +import tim.prune.data.Field; +import tim.prune.data.IntegerRange; +import tim.prune.data.Selection; +import tim.prune.data.TrackInfo; + +/** + * Class to hold point details and selection details + * as a visual component + */ +public class DetailsDisplay extends GenericDisplay +{ + // App object to be notified of editing commands + private App _app = null; + + // Track details + private JLabel _trackpointsLabel = null; + private JLabel _filenameLabel = null; + // Point details + private JLabel _indexLabel = null; + private JLabel _latLabel = null, _longLabel = null; + private JLabel _altLabel = null, _nameLabel = null; + private JLabel _timeLabel = null; + // Scroll bar + private JScrollBar _scroller = null; + private boolean _ignoreScrollEvents = false; + // Button panel + private JButton _startRangeButton = null, _endRangeButton = null; + private JButton _deletePointButton = null, _deleteRangeButton = null; + + // Range details + private JLabel _rangeLabel = null; + private JLabel _distanceLabel = null, _durationLabel = null; + private JLabel _altRangeLabel = null, _updownLabel = null; + // Units + private JComboBox _unitsDropdown = null; + // Formatter + private NumberFormat _distanceFormatter = NumberFormat.getInstance(); + + // Cached labels + private static final String LABEL_POINT_SELECTED1 = I18nManager.getText("details.index.selected") + ": "; + private static final String LABEL_POINT_LATITUDE = I18nManager.getText("fieldname.latitude") + ": "; + private static final String LABEL_POINT_LONGITUDE = I18nManager.getText("fieldname.longitude") + ": "; + private static final String LABEL_POINT_ALTITUDE = I18nManager.getText("fieldname.altitude") + ": "; + private static final String LABEL_POINT_TIMESTAMP = I18nManager.getText("fieldname.timestamp") + ": "; + private static final String LABEL_POINT_WAYPOINTNAME = I18nManager.getText("fieldname.waypointname") + ": "; + private static final String LABEL_RANGE_SELECTED1 = I18nManager.getText("details.range.selected") + ": "; + private static final String LABEL_RANGE_DURATION = I18nManager.getText("fieldname.duration") + ": "; + private static final String LABEL_RANGE_DISTANCE = I18nManager.getText("fieldname.distance") + ": "; + private static final String LABEL_RANGE_ALTITUDE = I18nManager.getText("fieldname.altitude") + ": "; + private static final String LABEL_RANGE_CLIMB = I18nManager.getText("details.range.climb") + ": "; + private static final String LABEL_RANGE_DESCENT = ", " + I18nManager.getText("details.range.descent") + ": "; + private static String LABEL_POINT_ALTITUDE_UNITS = null; + private static int LABEL_POINT_ALTITUDE_FORMAT = Altitude.FORMAT_NONE; + // scrollbar interval + private static final int SCROLLBAR_INTERVAL = 50; + + + /** + * Constructor + * @param inApp App object for callbacks + * @param inTrackInfo Track info object + */ + public DetailsDisplay(App inApp, TrackInfo inTrackInfo) + { + super(inTrackInfo); + _app = inApp; + setLayout(new BorderLayout()); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + // Track details panel + JPanel trackDetailsPanel = new JPanel(); + trackDetailsPanel.setLayout(new BoxLayout(trackDetailsPanel, BoxLayout.Y_AXIS)); + trackDetailsPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) + ); + JLabel trackDetailsLabel = new JLabel(I18nManager.getText("details.trackdetails")); + Font biggerFont = trackDetailsLabel.getFont(); + biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f); + trackDetailsLabel.setFont(biggerFont); + trackDetailsPanel.add(trackDetailsLabel); + _trackpointsLabel = new JLabel(I18nManager.getText("details.notrack")); + trackDetailsPanel.add(_trackpointsLabel); + _filenameLabel = new JLabel(""); + trackDetailsPanel.add(_filenameLabel); + + // Point details panel + JPanel pointDetailsPanel = new JPanel(); + pointDetailsPanel.setLayout(new BoxLayout(pointDetailsPanel, BoxLayout.Y_AXIS)); + pointDetailsPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) + ); + JLabel pointDetailsLabel = new JLabel(I18nManager.getText("details.pointdetails")); + pointDetailsLabel.setFont(biggerFont); + pointDetailsPanel.add(pointDetailsLabel); + _indexLabel = new JLabel(I18nManager.getText("details.nopointselection")); + pointDetailsPanel.add(_indexLabel); + _latLabel = new JLabel(""); + pointDetailsPanel.add(_latLabel); + _longLabel = new JLabel(""); + pointDetailsPanel.add(_longLabel); + _altLabel = new JLabel(""); + pointDetailsPanel.add(_altLabel); + _timeLabel = new JLabel(""); + pointDetailsPanel.add(_timeLabel); + _nameLabel = new JLabel(""); + pointDetailsPanel.add(_nameLabel); + pointDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // Scroll bar + _scroller = new JScrollBar(JScrollBar.HORIZONTAL, 0, SCROLLBAR_INTERVAL, 0, 100); + _scroller.addAdjustmentListener(new AdjustmentListener() { + public void adjustmentValueChanged(AdjustmentEvent e) + { + selectPoint(e.getValue()); + } + }); + _scroller.setEnabled(false); + + // Button panel + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new GridLayout(2, 2, 3, 3)); + _startRangeButton = new JButton(I18nManager.getText("button.startrange")); + _startRangeButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _trackInfo.getSelection().selectRangeStart(); + } + }); + _startRangeButton.setEnabled(false); + buttonPanel.add(_startRangeButton); + _endRangeButton = new JButton(I18nManager.getText("button.endrange")); + _endRangeButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _trackInfo.getSelection().selectRangeEnd(); + } + }); + _endRangeButton.setEnabled(false); + buttonPanel.add(_endRangeButton); + _deletePointButton = new JButton(I18nManager.getText("button.deletepoint")); + _deletePointButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _app.deleteCurrentPoint(); + } + }); + _deletePointButton.setEnabled(false); + buttonPanel.add(_deletePointButton); + _deleteRangeButton = new JButton(I18nManager.getText("button.deleterange")); + _deleteRangeButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _app.deleteSelectedRange(); + } + }); + _deleteRangeButton.setEnabled(false); + buttonPanel.add(_deleteRangeButton); + buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // range details panel + JPanel otherDetailsPanel = new JPanel(); + otherDetailsPanel.setLayout(new BoxLayout(otherDetailsPanel, BoxLayout.Y_AXIS)); + otherDetailsPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) + ); + + JLabel otherDetailsLabel = new JLabel(I18nManager.getText("details.rangedetails")); + otherDetailsLabel.setFont(biggerFont); + otherDetailsPanel.add(otherDetailsLabel); + _rangeLabel = new JLabel(I18nManager.getText("details.norangeselection")); + otherDetailsPanel.add(_rangeLabel); + _distanceLabel = new JLabel(""); + otherDetailsPanel.add(_distanceLabel); + _durationLabel = new JLabel(""); + otherDetailsPanel.add(_durationLabel); + _altRangeLabel = new JLabel(""); + otherDetailsPanel.add(_altRangeLabel); + _updownLabel = new JLabel(""); + otherDetailsPanel.add(_updownLabel); + otherDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // add the main panel at the top + add(mainPanel, BorderLayout.NORTH); + // add the slider, point details, and the other details to the main panel + mainPanel.add(buttonPanel); + mainPanel.add(Box.createVerticalStrut(5)); + mainPanel.add(_scroller); + mainPanel.add(Box.createVerticalStrut(5)); + mainPanel.add(trackDetailsPanel); + mainPanel.add(Box.createVerticalStrut(5)); + mainPanel.add(pointDetailsPanel); + mainPanel.add(Box.createVerticalStrut(5)); + mainPanel.add(otherDetailsPanel); + + // Add units selection + JPanel lowerPanel = new JPanel(); + lowerPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + lowerPanel.add(new JLabel(I18nManager.getText("details.distanceunits") + ": ")); + String[] distUnits = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.miles")}; + _unitsDropdown = new JComboBox(distUnits); + _unitsDropdown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + dataUpdated(); + } + }); + lowerPanel.add(_unitsDropdown); + add(lowerPanel, BorderLayout.SOUTH); + } + + + /** + * Select the specified point + * @param inValue value to select + */ + private void selectPoint(int inValue) + { + if (_track != null && !_ignoreScrollEvents) + { + _trackInfo.getSelection().selectPoint(inValue); + } + } + + + /** + * Notification that Track has been updated + */ + public void dataUpdated() + { + // Update track data + if (_track == null || _track.getNumPoints() <= 0) + { + _trackpointsLabel.setText(I18nManager.getText("details.notrack")); + _filenameLabel.setText(""); + } + else + { + _trackpointsLabel.setText(I18nManager.getText("details.track.points") + ": " + + _track.getNumPoints()); + int numFiles = _trackInfo.getFileInfo().getNumFiles(); + if (numFiles == 1) + { + _filenameLabel.setText(I18nManager.getText("details.track.file") + ": " + + _trackInfo.getFileInfo().getFilename()); + } + else if (numFiles > 1) + { + _filenameLabel.setText(I18nManager.getText("details.track.numfiles") + ": " + + numFiles); + } + else _filenameLabel.setText(""); + } + + // Update current point data, if any + DataPoint currentPoint = _trackInfo.getCurrentPoint(); + Selection selection = _trackInfo.getSelection(); + int currentPointIndex = selection.getCurrentPointIndex(); + if (_track == null || currentPoint == null) + { + _indexLabel.setText(I18nManager.getText("details.nopointselection")); + _latLabel.setText(""); + _longLabel.setText(""); + _altLabel.setText(""); + _timeLabel.setText(""); + _nameLabel.setText(""); + } + else + { + _indexLabel.setText(LABEL_POINT_SELECTED1 + + (currentPointIndex+1) + " " + I18nManager.getText("details.index.of") + + " " + _track.getNumPoints()); + _latLabel.setText(LABEL_POINT_LATITUDE + currentPoint.getLatitude().output(Coordinate.FORMAT_NONE)); + _longLabel.setText(LABEL_POINT_LONGITUDE + currentPoint.getLongitude().output(Coordinate.FORMAT_NONE)); + _altLabel.setText(LABEL_POINT_ALTITUDE + + (currentPoint.hasAltitude()? + (currentPoint.getAltitude().getValue() + getAltitudeUnitsLabel(currentPoint.getAltitude().getFormat())): + "")); + if (currentPoint.getTimestamp().isValid()) + _timeLabel.setText(LABEL_POINT_TIMESTAMP + currentPoint.getTimestamp().getText()); + else + _timeLabel.setText(""); + String name = currentPoint.getFieldValue(Field.WAYPT_NAME); + if (name != null && !name.equals("")) + { + _nameLabel.setText(LABEL_POINT_WAYPOINTNAME + name); + } + else _nameLabel.setText(""); + } + + // Update scroller settings + _ignoreScrollEvents = true; + if (_track == null || _track.getNumPoints() < 2) + { + // careful to avoid event loops here + // _scroller.setValue(0); + _scroller.setEnabled(false); + } + else + { + _scroller.setMaximum(_track.getNumPoints() + SCROLLBAR_INTERVAL); + if (currentPointIndex >= 0) + _scroller.setValue(currentPointIndex); + _scroller.setEnabled(true); + } + _ignoreScrollEvents = false; + + // Update button panel + boolean hasPoint = (_track != null && currentPointIndex >= 0); + _startRangeButton.setEnabled(hasPoint); + _endRangeButton.setEnabled(hasPoint); + _deletePointButton.setEnabled(hasPoint); + _deleteRangeButton.setEnabled(selection.hasRangeSelected()); + + // Update range details + if (_track == null || !selection.hasRangeSelected()) + { + _rangeLabel.setText(I18nManager.getText("details.norangeselection")); + _distanceLabel.setText(""); + _durationLabel.setText(""); + _altRangeLabel.setText(""); + _updownLabel.setText(""); + } + else + { + _rangeLabel.setText(LABEL_RANGE_SELECTED1 + + (selection.getStart()+1) + " " + I18nManager.getText("details.range.to") + + " " + (selection.getEnd()+1)); + if (_unitsDropdown.getSelectedIndex() == 0) + _distanceLabel.setText(LABEL_RANGE_DISTANCE + buildDistanceString( + selection.getDistance(Distance.UNITS_KILOMETRES)) + + " " + I18nManager.getText("units.kilometres.short")); + else + _distanceLabel.setText(LABEL_RANGE_DISTANCE + buildDistanceString( + selection.getDistance(Distance.UNITS_MILES)) + + " " + I18nManager.getText("units.miles.short")); + if (selection.getNumSeconds() > 0) + _durationLabel.setText(LABEL_RANGE_DURATION + buildDurationString(selection.getNumSeconds())); + else + _durationLabel.setText(""); + String altUnitsLabel = getAltitudeUnitsLabel(selection.getAltitudeFormat()); + IntegerRange altRange = selection.getAltitudeRange(); + if (altRange.getMinimum() >= 0 && altRange.getMaximum() >= 0) + { + _altRangeLabel.setText(LABEL_RANGE_ALTITUDE + + altRange.getMinimum() + altUnitsLabel + " " + + I18nManager.getText("details.altitude.to") + " " + + altRange.getMaximum() + altUnitsLabel); + _updownLabel.setText(LABEL_RANGE_CLIMB + selection.getClimb() + altUnitsLabel + + LABEL_RANGE_DESCENT + selection.getDescent() + altUnitsLabel); + } + else + { + _altRangeLabel.setText(""); + _updownLabel.setText(""); + } + } + } + + + /** + * Choose the appropriate altitude units label for the specified format + * @param inFormat altitude format + * @return language-sensitive string + */ + private static String getAltitudeUnitsLabel(int inFormat) + { + if (inFormat == LABEL_POINT_ALTITUDE_FORMAT && LABEL_POINT_ALTITUDE_UNITS != null) + return LABEL_POINT_ALTITUDE_UNITS; + LABEL_POINT_ALTITUDE_FORMAT = inFormat; + if (inFormat == Altitude.FORMAT_METRES) + return " " + I18nManager.getText("units.metres.short"); + return " " + I18nManager.getText("units.feet.short"); + } + + + /** + * Build a String to describe a time duration + * @param inNumSecs number of seconds + * @return time as a string, days, hours, mins, secs as appropriate + */ + private static String buildDurationString(long inNumSecs) + { + if (inNumSecs <= 0L) return ""; + if (inNumSecs < 60L) return "" + inNumSecs + I18nManager.getText("display.range.time.secs"); + if (inNumSecs < 3600L) return "" + (inNumSecs / 60) + I18nManager.getText("display.range.time.mins") + + " " + (inNumSecs % 60) + I18nManager.getText("display.range.time.secs"); + if (inNumSecs < 86400L) return "" + (inNumSecs / 60 / 60) + I18nManager.getText("display.range.time.hours") + + " " + ((inNumSecs / 60) % 60) + I18nManager.getText("display.range.time.mins"); + if (inNumSecs < 8640000L) return "" + (inNumSecs / 86400L) + I18nManager.getText("display.range.time.days"); + return "big"; + } + + + /** + * Build a String to describe a distance + * @param inDist distance + * @return formatted String + */ + private String buildDistanceString(double inDist) + { + // Set precision of formatter + int numDigits = 0; + if (inDist < 1.0) + numDigits = 3; + else if (inDist < 10.0) + numDigits = 2; + else if (inDist < 100.0) + numDigits = 1; + // set formatter + _distanceFormatter.setMaximumFractionDigits(numDigits); + _distanceFormatter.setMinimumFractionDigits(numDigits); + return _distanceFormatter.format(inDist); + } +} diff --git a/tim/prune/gui/GenericChart.java b/tim/prune/gui/GenericChart.java new file mode 100644 index 0000000..081c823 --- /dev/null +++ b/tim/prune/gui/GenericChart.java @@ -0,0 +1,103 @@ +package tim.prune.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import tim.prune.I18nManager; +import tim.prune.data.TrackInfo; + + +/** + * Generic chart component to form baseclass for map and profile charts + */ +public abstract class GenericChart extends GenericDisplay implements MouseListener +{ + protected Dimension MINIMUM_SIZE = new Dimension(200, 250); + protected static final int BORDER_WIDTH = 8; + + // Colours + private static final Color COLOR_BORDER_BG = Color.GRAY; + private static final Color COLOR_CHART_BG = Color.WHITE; + private static final Color COLOR_CHART_LINE = Color.BLACK; + private static final Color COLOR_NODATA_TEXT = Color.GRAY; + + + /** + * Constructor + * @param inTrackInfo track info object + */ + protected GenericChart(TrackInfo inTrackInfo) + { + super(inTrackInfo); + } + + /** + * Override minimum size method to restrict map + */ + public Dimension getMinimumSize() + { + return MINIMUM_SIZE; + } + + /** + * Override paint method to draw map + */ + public void paint(Graphics g) + { + super.paint(g); + int width = getWidth(); + int height = getHeight(); + // border background + g.setColor(COLOR_BORDER_BG); + g.fillRect(0, 0, width, height); + if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return; + // blank graph area, with line border + g.setColor(COLOR_CHART_BG); + g.fillRect(BORDER_WIDTH, BORDER_WIDTH, width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH); + g.setColor(COLOR_CHART_LINE); + g.drawRect(BORDER_WIDTH, BORDER_WIDTH, width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH); + // Display message if no data to be displayed + if (_track == null || _track.getNumPoints() <= 0) + { + g.setColor(COLOR_NODATA_TEXT); + g.drawString(I18nManager.getText("display.nodata"), 50, height/2); + } + } + + + /** + * Method to inform map that data has changed + */ + public void dataUpdated() + { + repaint(); + } + + + /** + * mouse enter events ignored + */ + public void mouseEntered(MouseEvent e) + {} + + /** + * mouse exit events ignored + */ + public void mouseExited(MouseEvent e) + {} + + /** + * ignore mouse pressed for now too + */ + public void mousePressed(MouseEvent e) + {} + + /** + * and also ignore mouse released + */ + public void mouseReleased(MouseEvent e) + {} +} diff --git a/tim/prune/gui/GenericDisplay.java b/tim/prune/gui/GenericDisplay.java new file mode 100644 index 0000000..67c4d92 --- /dev/null +++ b/tim/prune/gui/GenericDisplay.java @@ -0,0 +1,26 @@ +package tim.prune.gui; + +import javax.swing.JPanel; + +import tim.prune.DataSubscriber; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; + +/** + * Superclass of all display components + */ +public abstract class GenericDisplay extends JPanel implements DataSubscriber +{ + protected TrackInfo _trackInfo = null; + protected Track _track = null; + + /** + * Constructor + * @param inTrackInfo trackInfo object + */ + public GenericDisplay(TrackInfo inTrackInfo) + { + _trackInfo = inTrackInfo; + _track = _trackInfo.getTrack(); + } +} diff --git a/tim/prune/gui/MapChart.java b/tim/prune/gui/MapChart.java new file mode 100644 index 0000000..13b1b7d --- /dev/null +++ b/tim/prune/gui/MapChart.java @@ -0,0 +1,581 @@ +package tim.prune.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Field; +import tim.prune.data.TrackInfo; + + +/** + * Display component for the main map + */ +public class MapChart extends GenericChart implements MouseWheelListener, KeyListener, MouseMotionListener +{ + // Constants + private static final int POINT_RADIUS = 4; + private static final int CLICK_SENSITIVITY = 10; + private static final double ZOOM_SCALE_FACTOR = 1.2; + private static final int PAN_DISTANCE = 10; + private static final int LIMIT_WAYPOINT_NAMES = 40; + + // Colours + private static final Color COLOR_BG = Color.WHITE; + private static final Color COLOR_POINT = Color.BLUE; + private static final Color COLOR_CURR_RANGE = Color.GREEN; + private static final Color COLOR_CROSSHAIRS = Color.RED; + private static final Color COLOR_WAYPT_NAME = Color.BLACK; + + // Instance variables + private App _app = null; + private BufferedImage _image = null; + private JPopupMenu _popup = null; + private JCheckBoxMenuItem _autoPanMenuItem = null; + private String _trackString = null; + private int _numPoints = -1; + private double _scale; + private double _offsetX, _offsetY, _zoomScale; + private int _lastSelectedPoint = -1; + private int _dragStartX = -1, _dragStartY = -1; + private int _zoomDragFromX = -1, _zoomDragFromY = -1; + private int _zoomDragToX = -1, _zoomDragToY = -1; + private boolean _zoomDragging = false; + + + /** + * Constructor + * @param inApp App object for callbacks + * @param inTrackInfo track info object + */ + public MapChart(App inApp, TrackInfo inTrackInfo) + { + super(inTrackInfo); + _app = inApp; + makePopup(); + addMouseListener(this); + addMouseWheelListener(this); + addMouseMotionListener(this); + setFocusable(true); + addKeyListener(this); + MINIMUM_SIZE = new Dimension(200, 250); + _zoomScale = 1.0; + } + + + /** + * Override track updating to refresh image + */ + public void dataUpdated() + { + // Check if number of points has changed or Track + // object has a different signature + if (_track.getNumPoints() != _numPoints) + { + _image = null; + _numPoints = _track.getNumPoints(); + } + super.dataUpdated(); + } + + + /** + * Override paint method to draw map + */ + public void paint(Graphics g) + { + if (_track == null) + { + super.paint(g); + return; + } + + int width = getWidth(); + int height = getHeight(); + int x, y; + + // Find x and y ranges, and scale to fit + double scaleX = (_track.getXRange().getMaximum() - _track.getXRange().getMinimum()) + / (width - 2 * (BORDER_WIDTH + POINT_RADIUS)); + double scaleY = (_track.getYRange().getMaximum() - _track.getYRange().getMinimum()) + / (height - 2 * (BORDER_WIDTH + POINT_RADIUS)); + _scale = scaleX; + if (scaleY > _scale) _scale = scaleY; + + // Autopan if necessary + int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex(); + if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint) + { + // Autopan is enabled and a point is selected - work out x and y to see if it's within range + x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale); + y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale); + if (x < BORDER_WIDTH) + { + // autopan left + _offsetX -= (width / 4 - x) * _scale / _zoomScale; + _image = null; + } + else if (x > (width - BORDER_WIDTH)) + { + // autopan right + _offsetX += (x - width * 3/4) * _scale / _zoomScale; + _image = null; + } + if (y < BORDER_WIDTH) + { + // autopan up + _offsetY += (height / 4 - y) * _scale / _zoomScale; + _image = null; + } + else if (y > (height - BORDER_WIDTH)) + { + // autopan down + _offsetY -= (y - height * 3/4) * _scale / _zoomScale; + _image = null; + } + } + _lastSelectedPoint = selectedPoint; + + if (_image == null || width != _image.getWidth() || height != _image.getHeight()) + { + createBackgroundImage(); + } + // draw buffered image onto g + g.drawImage(_image, 0, 0, width, height, COLOR_BG, null); + + // draw selected range, if any + if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging) + { + int rangeStart = _trackInfo.getSelection().getStart(); + int rangeEnd = _trackInfo.getSelection().getEnd(); + g.setColor(COLOR_CURR_RANGE); + for (int i=rangeStart; i<=rangeEnd; i++) + { + x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale); + y = height/2 - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale); + if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH) + && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH) + { + g.drawOval(x - 2, y - 2, 4, 4); + } + } + } + + // Highlight selected point + if (selectedPoint >= 0 && !_zoomDragging) + { + g.setColor(COLOR_CROSSHAIRS); + x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale); + y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale); + if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH) + && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH) + { + // Draw cross-hairs for current point + g.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH); + g.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y); + + // Show selected point afterwards to make sure it's on top + g.drawOval(x - 2, y - 2, 4, 4); + g.drawOval(x - 3, y - 3, 6, 6); + } + } + + if (_zoomDragging) + { + g.setColor(COLOR_CROSSHAIRS); + g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY); + g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY); + g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY); + g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY); + } + } + + + /** + * Draw the map onto an offscreen image + */ + private void createBackgroundImage() + { + int width = getWidth(); + int height = getHeight(); + int x, y; + // Make a new image and initialise it + _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics bufferedG = _image.getGraphics(); + super.paint(bufferedG); + + // Loop and show all points + int numPoints = _track.getNumPoints(); + bufferedG.setColor(COLOR_POINT); + int halfWidth = width/2; + int halfHeight = height/2; + for (int i=0; i BORDER_WIDTH && x < (width - BORDER_WIDTH) + && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH) + { + bufferedG.drawOval(x - 2, y - 2, 4, 4); + } + } + + // Loop again and show waypoints with names + bufferedG.setColor(COLOR_WAYPT_NAME); + FontMetrics fm = bufferedG.getFontMetrics(); + int nameHeight = fm.getHeight(); + int numWaypointNamesShown = 0; + for (int i=0; i BORDER_WIDTH && x < (width - BORDER_WIDTH) + && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH) + { + bufferedG.fillOval(x - 3, y - 3, 6, 6); + // Figure out where to draw name so it doesn't obscure track + int nameWidth = fm.stringWidth(waypointName); + if (nameWidth < (width - 2 * BORDER_WIDTH)) + { + double nameAngle = 0.3; + double nameRadius = 1.0; + boolean drawnName = false; + while (!drawnName) + { + int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2); + int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2); + if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH) + && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH) + { + // name can fit in grid - does it overlap data points? + if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0) + { + bufferedG.drawString(waypointName, nameX, nameY); + drawnName = true; + numWaypointNamesShown++; + } + } + nameAngle += 0.08; + nameRadius += 0.2; + // wasn't room within the radius, so don't print name + if (nameRadius > 50.0) + { + drawnName = true; + } + } + } + } + } + } + } + + + /** + * Tests whether there are any data points within the specified x,y rectangle + * @param inX left X coordinate + * @param inY bottom Y coordinate + * @param inWidth width of rectangle + * @param inHeight height of rectangle + * @return true if there's at least one data point in the rectangle + */ + private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight) + { + // if (true) return true; + for (int x=0; x BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH) + && yClick < (getHeight() - BORDER_WIDTH)) + { + // Check left click or right click + if (e.isMetaDown()) + { + // Only show popup if track has data + if (_track != null && _track.getNumPoints() > 0) + _popup.show(this, e.getX(), e.getY()); + } + else + { + // Find point within range of click point + double pointX = (xClick - getWidth()/2) * _scale / _zoomScale + _offsetX; + double pointY = (getHeight()/2 - yClick) * _scale / _zoomScale + _offsetY; + int selectedPointIndex = _track.getNearestPointIndex( + pointX, pointY, CLICK_SENSITIVITY * _scale, false); + // Select the given point (or deselect if no point was found) + _trackInfo.getSelection().selectPoint(selectedPointIndex); + } + } + } + } + + + /** + * Respond to mouse released to reset dragging + * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) + */ + public void mouseReleased(MouseEvent e) + { + _dragStartX = _dragStartY = -1; + if (e.isMetaDown()) + { + if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0) + { + // zoom area marked out - calculate offset and zoom + int xPan = (getWidth() - _zoomDragFromX - e.getX()) / 2; + int yPan = (getHeight() - _zoomDragFromY - e.getY()) / 2; + double xZoom = Math.abs(getWidth() * 1.0 / (e.getX() - _zoomDragFromX)); + double yZoom = Math.abs(getHeight() * 1.0 / (e.getY() - _zoomDragFromY)); + double extraZoom = (xZoom>yZoom?yZoom:xZoom); + // Pan first to ensure pan occurs with correct scale + panMap(yPan, xPan); + // Then zoom in and request repaint + _zoomScale = _zoomScale * extraZoom; + _image = null; + repaint(); + } + _zoomDragFromX = _zoomDragFromY = -1; + _zoomDragging = false; + } + } + + + /** + * Respond to mouse wheel events to zoom the map + * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent) + */ + public void mouseWheelMoved(MouseWheelEvent e) + { + zoomMap(e.getWheelRotation() < 0); + } + + + /** + * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) + */ + public void keyPressed(KeyEvent e) + { + int code = e.getKeyCode(); + // Check for meta key + if (e.isControlDown()) + { + // Check for arrow keys to zoom in and out + if (code == KeyEvent.VK_UP) + zoomMap(true); + else if (code == KeyEvent.VK_DOWN) + zoomMap(false); + // Key nav for next/prev point + else if (code == KeyEvent.VK_LEFT) + _trackInfo.getSelection().selectPreviousPoint(); + else if (code == KeyEvent.VK_RIGHT) + _trackInfo.getSelection().selectNextPoint(); + } + else + { + // Check for arrow keys to pan + int upwardsPan = 0; + if (code == KeyEvent.VK_UP) + upwardsPan = PAN_DISTANCE; + else if (code == KeyEvent.VK_DOWN) + upwardsPan = -PAN_DISTANCE; + int rightwardsPan = 0; + if (code == KeyEvent.VK_RIGHT) + rightwardsPan = -PAN_DISTANCE; + else if (code == KeyEvent.VK_LEFT) + rightwardsPan = PAN_DISTANCE; + panMap(upwardsPan, rightwardsPan); + // Check for delete key to delete current point + if (code == KeyEvent.VK_DELETE && _trackInfo.getSelection().getCurrentPointIndex() >= 0) + _app.deleteCurrentPoint(); + } + } + + + /** + * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) + */ + public void keyReleased(KeyEvent e) + { + // ignore + } + + + /** + * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) + */ + public void keyTyped(KeyEvent e) + { + // ignore + } + + + /** + * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent) + */ + public void mouseDragged(MouseEvent e) + { + if (!e.isMetaDown()) + { + if (_dragStartX > 0) + { + int xShift = e.getX() - _dragStartX; + int yShift = e.getY() - _dragStartY; + panMap(yShift, xShift); + } + _dragStartX = e.getX(); + _dragStartY = e.getY(); + } + else + { + // Right click-and-drag for zoom + if (_zoomDragFromX < 0 || _zoomDragFromY < 0) + { + _zoomDragFromX = e.getX(); + _zoomDragFromY = e.getY(); + } + else + { + _zoomDragToX = e.getX(); + _zoomDragToY = e.getY(); + _zoomDragging = true; + } + repaint(); + } + } + + + /** + * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent) + */ + public void mouseMoved(MouseEvent e) + { + // ignore + } +} diff --git a/tim/prune/gui/MenuManager.java b/tim/prune/gui/MenuManager.java new file mode 100644 index 0000000..665142e --- /dev/null +++ b/tim/prune/gui/MenuManager.java @@ -0,0 +1,337 @@ +package tim.prune.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; + +import tim.prune.App; +import tim.prune.DataSubscriber; +import tim.prune.I18nManager; +import tim.prune.data.Selection; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; + +/** + * Class to manage the menu bar, + * including enabling and disabling the menu items + */ +public class MenuManager implements DataSubscriber +{ + private JFrame _parent = null; + private App _app = null; + private Track _track = null; + private Selection _selection = null; + + // Menu items which need enabling/disabling + JMenuItem _saveItem = null; + JMenuItem _exportItem = null; + JMenuItem _undoItem = null; + JMenuItem _clearUndoItem = null; + JMenuItem _deletePointItem = null; + JMenuItem _deleteRangeItem = null; + JMenuItem _deleteDuplicatesItem = null; + JMenuItem _compressItem = null; + JMenuItem _interpolateItem = null; + JMenuItem _selectAllItem = null; + JMenuItem _selectNoneItem = null; + JMenuItem _show3dItem = null; + JMenuItem _reverseItem = null; + JMenu _rearrangeMenu = null; + JMenuItem _rearrangeStartItem = null; + JMenuItem _rearrangeEndItem = null; + JMenuItem _rearrangeNearestItem = null; + + + /** + * Constructor + * @param inParent parent object for dialogs + * @param inApp application to call on menu actions + */ + public MenuManager(JFrame inParent, App inApp, TrackInfo inTrackInfo) + { + _parent = inParent; + _app = inApp; + _track = inTrackInfo.getTrack(); + _selection = inTrackInfo.getSelection(); + } + + + /** + * Create a JMenuBar containing all menu items + * @return JMenuBar + */ + public JMenuBar createMenuBar() + { + JMenuBar menubar = new JMenuBar(); + JMenu fileMenu = new JMenu(I18nManager.getText("menu.file")); + JMenuItem openMenuItem = new JMenuItem(I18nManager.getText("menu.file.open")); + openMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); + openMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.openFile(); + } + }); + fileMenu.add(openMenuItem); + _saveItem = new JMenuItem(I18nManager.getText("menu.file.save"), KeyEvent.VK_S); + _saveItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.saveFile(); + } + }); + _saveItem.setEnabled(false); + fileMenu.add(_saveItem); + // Export + _exportItem = new JMenuItem(I18nManager.getText("menu.file.export")); + _exportItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.exportKml(); + } + }); + _exportItem.setEnabled(false); + fileMenu.add(_exportItem); + fileMenu.addSeparator(); + JMenuItem exitMenuItem = new JMenuItem(I18nManager.getText("menu.file.exit")); + exitMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.exit(); + } + }); + fileMenu.add(exitMenuItem); + menubar.add(fileMenu); + JMenu editMenu = new JMenu(I18nManager.getText("menu.edit")); + editMenu.setMnemonic(KeyEvent.VK_E); + _undoItem = new JMenuItem(I18nManager.getText("menu.edit.undo")); + _undoItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.beginUndo(); + } + }); + _undoItem.setEnabled(false); + editMenu.add(_undoItem); + _clearUndoItem = new JMenuItem(I18nManager.getText("menu.edit.clearundo")); + _clearUndoItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.clearUndo(); + } + }); + _clearUndoItem.setEnabled(false); + editMenu.add(_clearUndoItem); + editMenu.addSeparator(); + _deletePointItem = new JMenuItem(I18nManager.getText("menu.edit.deletepoint")); + _deletePointItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.deleteCurrentPoint(); + } + }); + _deletePointItem.setEnabled(false); + editMenu.add(_deletePointItem); + _deleteRangeItem = new JMenuItem(I18nManager.getText("menu.edit.deleterange")); + _deleteRangeItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.deleteSelectedRange(); + } + }); + _deleteRangeItem.setEnabled(false); + editMenu.add(_deleteRangeItem); + _deleteDuplicatesItem = new JMenuItem(I18nManager.getText("menu.edit.deleteduplicates")); + _deleteDuplicatesItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.deleteDuplicates(); + } + }); + _deleteDuplicatesItem.setEnabled(false); + editMenu.add(_deleteDuplicatesItem); + _compressItem = new JMenuItem(I18nManager.getText("menu.edit.compress")); + _compressItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.compressTrack(); + } + }); + _compressItem.setEnabled(false); + editMenu.add(_compressItem); + editMenu.addSeparator(); + _interpolateItem = new JMenuItem(I18nManager.getText("menu.edit.interpolate")); + _interpolateItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.interpolateSelection(); + } + }); + _interpolateItem.setEnabled(false); + editMenu.add(_interpolateItem); + _reverseItem = new JMenuItem(I18nManager.getText("menu.edit.reverse")); + _reverseItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.reverseRange(); + } + }); + _reverseItem.setEnabled(false); + editMenu.add(_reverseItem); + // Rearrange waypoints + _rearrangeMenu = new JMenu(I18nManager.getText("menu.edit.rearrange")); + _rearrangeMenu.setEnabled(false); + _rearrangeStartItem = new JMenuItem(I18nManager.getText("menu.edit.rearrange.start")); + _rearrangeStartItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.rearrangeWaypoints(App.REARRANGE_TO_START); + } + }); + _rearrangeStartItem.setEnabled(true); + _rearrangeMenu.add(_rearrangeStartItem); + _rearrangeEndItem = new JMenuItem(I18nManager.getText("menu.edit.rearrange.end")); + _rearrangeEndItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.rearrangeWaypoints(App.REARRANGE_TO_END); + } + }); + _rearrangeEndItem.setEnabled(true); + _rearrangeMenu.add(_rearrangeEndItem); + _rearrangeNearestItem = new JMenuItem(I18nManager.getText("menu.edit.rearrange.nearest")); + _rearrangeNearestItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.rearrangeWaypoints(App.REARRANGE_TO_NEAREST); + } + }); + _rearrangeNearestItem.setEnabled(true); + _rearrangeMenu.add(_rearrangeNearestItem); + editMenu.add(_rearrangeMenu); + menubar.add(editMenu); + + // Select menu + JMenu selectMenu = new JMenu(I18nManager.getText("menu.select")); + _selectAllItem = new JMenuItem(I18nManager.getText("menu.select.all")); + _selectAllItem.setEnabled(false); + _selectAllItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.selectAll(); + } + }); + selectMenu.add(_selectAllItem); + _selectNoneItem = new JMenuItem(I18nManager.getText("menu.select.none")); + _selectNoneItem.setEnabled(false); + _selectNoneItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.selectNone(); + } + }); + selectMenu.add(_selectNoneItem); + menubar.add(selectMenu); + + // Add 3d menu if available + if (isJava3dEnabled()) + { + JMenu threeDMenu = new JMenu(I18nManager.getText("menu.3d")); + _show3dItem = new JMenuItem(I18nManager.getText("menu.3d.show3d")); + _show3dItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _app.show3dWindow(); + } + }); + _show3dItem.setEnabled(false); + threeDMenu.add(_show3dItem); + menubar.add(threeDMenu); + } + + // Help menu for About + JMenu helpMenu = new JMenu(I18nManager.getText("menu.help")); + JMenuItem aboutItem = new JMenuItem(I18nManager.getText("menu.help.about")); + aboutItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + new AboutScreen(_parent).show(); + } + }); + helpMenu.add(aboutItem); + menubar.add(helpMenu); + + return menubar; + } + + + /** + * Method to update menu when file loaded + */ + public void informFileLoaded() + { + // save, undo, delete enabled + _saveItem.setEnabled(true); + _undoItem.setEnabled(true); + _deleteDuplicatesItem.setEnabled(true); + _compressItem.setEnabled(true); + } + + + /** + * @return true if 3d capability is installed + */ + private static boolean isJava3dEnabled() + { + boolean has3d = false; + try + { + Class universeClass = Class.forName("com.sun.j3d.utils.universe.SimpleUniverse"); + has3d = true; + } + catch (ClassNotFoundException e) + { + // no java3d classes available + } + return has3d; + } + + + /** + * @see tim.prune.DataSubscriber#dataUpdated(tim.prune.data.Track) + */ + public void dataUpdated() + { + boolean hasData = (_track != null && _track.getNumPoints() > 0); + // set functions which require data + _saveItem.setEnabled(hasData); + _exportItem.setEnabled(hasData); + _deleteDuplicatesItem.setEnabled(hasData); + _compressItem.setEnabled(hasData); + _rearrangeMenu.setEnabled(hasData && _track.hasMixedData()); + _selectAllItem.setEnabled(hasData); + _selectNoneItem.setEnabled(hasData); + if (_show3dItem != null) + _show3dItem.setEnabled(hasData); + // is undo available? + boolean hasUndo = !_app.getUndoStack().isEmpty(); + _undoItem.setEnabled(hasUndo); + _clearUndoItem.setEnabled(hasUndo); + // is there a current point? + boolean hasPoint = (hasData && _selection.getCurrentPointIndex() >= 0); + _deletePointItem.setEnabled(hasPoint); + // is there a current range? + boolean hasRange = (hasData && _selection.hasRangeSelected()); + _deleteRangeItem.setEnabled(hasRange); + _interpolateItem.setEnabled(hasRange + && (_selection.getEnd() - _selection.getStart()) == 1); + _reverseItem.setEnabled(hasRange); + } +} diff --git a/tim/prune/gui/ProfileChart.java b/tim/prune/gui/ProfileChart.java new file mode 100644 index 0000000..431da0d --- /dev/null +++ b/tim/prune/gui/ProfileChart.java @@ -0,0 +1,207 @@ +package tim.prune.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseEvent; + +import tim.prune.I18nManager; +import tim.prune.data.AltitudeRange; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; + +/** + * Chart component for the profile display + */ +public class ProfileChart extends GenericChart +{ + private double _xScaleFactor = 0.0; + private static final int[] ALTITUDE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50}; + private static final Color COLOR_LINES = Color.GRAY; + private static final Color COLOR_ALT_BARS = Color.BLUE; + private static final Color COLOR_SELECTED = Color.RED; + private static final Color COLOR_SELECTED_BG = Color.ORANGE; + private static final Color COLOR_ALT_SCALE = Color.RED; + + + /** + * Constructor + * @param inTrackInfo Track info object + */ + public ProfileChart(TrackInfo inTrackInfo) + { + super(inTrackInfo); + MINIMUM_SIZE = new Dimension(200, 100); + addMouseListener(this); + } + + + /** + * Override paint method to draw map + */ + public void paint(Graphics g) + { + super.paint(g); + if (_track != null && _track.getNumPoints() > 0) + { + int width = getWidth(); + int height = getHeight(); + AltitudeRange altitudeRange = _track.getAltitudeRange(); + int minAltitude = altitudeRange.getMinimum(); + int maxAltitude = altitudeRange.getMaximum(); + + // message if no altitudes in track + if (minAltitude < 0 || maxAltitude < 0) + { + g.setColor(COLOR_LINES); + g.drawString(I18nManager.getText("display.noaltitudes"), 50, height/2); + return; + } + + // altitude profile + int numPoints = _track.getNumPoints(); + _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH) / numPoints; + double yScaleFactor = 1.0 * (height - 2 * BORDER_WIDTH) / + (altitudeRange.getMaximum() - minAltitude); + int barWidth = (int) (_xScaleFactor + 1.0); + int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex(); + + // horizontal lines for scale - set to round numbers eg 500m + int lineScale = getLineScale(minAltitude, maxAltitude); + int altitude = 0; + int y = 0; + if (lineScale > 1) + { + g.setColor(COLOR_LINES); + while (altitude < maxAltitude) + { + if (altitude > minAltitude) + { + y = height - BORDER_WIDTH - (int) (yScaleFactor * (altitude - minAltitude)); + g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y); + } + altitude += lineScale; + } + } + + // loop through points + int chartFormat = altitudeRange.getFormat(); + for (int p = 0; p < numPoints; p++) + { + int x = (int) (_xScaleFactor * p); + if (p == selectedPoint) + { + g.setColor(COLOR_SELECTED_BG); + g.fillRect(BORDER_WIDTH + x, BORDER_WIDTH+1, barWidth, height-2*BORDER_WIDTH-2); + g.setColor(COLOR_SELECTED); + } + else + { + g.setColor(COLOR_ALT_BARS); + } + if (_track.getPoint(p).getAltitude().isValid()) + { + altitude = _track.getPoint(p).getAltitude().getValue(chartFormat); + y = (int) (yScaleFactor * (altitude - minAltitude)); + g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y); + } + } + // Draw numbers on top of the graph to mark scale + if (lineScale > 1) + { + int textHeight = g.getFontMetrics().getHeight(); + altitude = 0; + y = 0; + g.setColor(COLOR_ALT_SCALE); + while (altitude < maxAltitude) + { + if (altitude > minAltitude) + { + y = height - BORDER_WIDTH - (int) (yScaleFactor * (altitude - minAltitude)); + // Limit y so String isn't above border + if (y < (BORDER_WIDTH + textHeight)) + { + y = BORDER_WIDTH + textHeight; + } + g.drawString(""+altitude, BORDER_WIDTH + 5, y); + } + altitude += lineScale; + } + } + } + } + + + /** + * Work out the scale for the horizontal lines + * @param inMin min altitude of data + * @param inMax max altitude of data + * @return scale separation, or -1 for no scale + */ + private int getLineScale(int inMin, int inMax) + { + if ((inMax - inMin) < 50 || inMax < 0) + { + return -1; + } + int numScales = ALTITUDE_SCALES.length; + int scale = 0; + int numLines = 0; + int altitude = 0; + for (int i=0; i inMin) + { + numLines++; + } + } + if (numLines > 2) + { + return scale; + } + } + } + // no suitable scale found so just use minimum + return ALTITUDE_SCALES[numScales-1]; + } + + + /** + * Method to inform map that data has changed + */ + public void dataUpdated(Track inTrack) + { + _track = inTrack; + repaint(); + } + + + /** + * React to click on profile display + */ + public void mouseClicked(MouseEvent e) + { + // ignore right clicks + if (_track != null && !e.isMetaDown()) + { + int xClick = e.getX(); + int yClick = e.getY(); + // Check click is within main area (not in border) + if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH) + && yClick < (getHeight() - BORDER_WIDTH)) + { + // work out which data point is nearest and select it + int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor); + _trackInfo.getSelection().selectPoint(pointNum); + } + } + } +} diff --git a/tim/prune/gui/UndoManager.java b/tim/prune/gui/UndoManager.java new file mode 100644 index 0000000..5c3a992 --- /dev/null +++ b/tim/prune/gui/UndoManager.java @@ -0,0 +1,95 @@ +package tim.prune.gui; + +import java.awt.FlowLayout; +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Stack; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.undo.UndoOperation; + +/** + * Class to manage the selection of actions to undo + */ +public class UndoManager +{ + private App _app; + private JDialog _dialog; + private JList _actionList; + + + /** + * Constructor + */ + public UndoManager(App inApp, JFrame inFrame) + { + _app = inApp; + _dialog = new JDialog(inFrame, I18nManager.getText("dialog.undo.title"), true); + _dialog.setLocationRelativeTo(inFrame); + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout(3, 3)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + Stack undoStack = inApp.getUndoStack(); + mainPanel.add(new JLabel(I18nManager.getText("dialog.undo.pretext")), BorderLayout.NORTH); + + String[] undoActions = new String[undoStack.size()]; + for (int i=0; i 0) + { + _actionList.setSelectionInterval(0, _actionList.getMaxSelectionIndex()); + } + } + }); + mainPanel.add(new JScrollPane(_actionList), BorderLayout.CENTER); + // Buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton okButton = new JButton("OK"); + okButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _app.undoActions(_actionList.getMaxSelectionIndex() + 1); + _dialog.dispose(); + } + }); + buttonPanel.add(okButton); + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + _dialog.getContentPane().add(mainPanel); + _dialog.pack(); + _dialog.show(); + } + +} diff --git a/tim/prune/lang/prune-texts.properties b/tim/prune/lang/prune-texts.properties new file mode 100644 index 0000000..01f9dad --- /dev/null +++ b/tim/prune/lang/prune-texts.properties @@ -0,0 +1,187 @@ +# Text entries for the Prune application +# English entries as default - others can be added + +# Menu entries +menu.file=File +menu.file.open=Open +menu.file.save=Save +menu.file.export=Export KML +menu.file.exit=Exit +menu.edit=Edit +menu.edit.undo=Undo +menu.edit.clearundo=Clear undo list +menu.edit.deletepoint=Delete point +menu.edit.deleterange=Delete range +menu.edit.deleteduplicates=Delete duplicates +menu.edit.compress=Compress track +menu.edit.interpolate=Interpolate +menu.edit.reverse=Reverse range +menu.edit.rearrange=Rearrange waypoints +menu.edit.rearrange.start=All to start of file +menu.edit.rearrange.end=All to end of file +menu.edit.rearrange.nearest=Each to nearest track point +menu.select=Select +menu.select.all=Select all +menu.select.none=Select none +menu.3d=Three-D +menu.3d.show3d=Show in Three-D +menu.help=Help +menu.help.about=About Prune +# Popup menu for map +menu.map.zoomin=Zoom in +menu.map.zoomout=Zoom out +menu.map.zoomfull=Zoom to full scale +menu.map.autopan=Autopan + +# Dialogs +dialog.exit.confirm.title=Exit prune +dialog.exit.confirm.text=Your data is not saved. Are you sure you want to exit? +dialog.openappend.title=Append to existing data +dialog.openappend.text=Append this data to the data already loaded? +dialog.deleteduplicates.title=Delete Duplicates +dialog.deleteduplicates.single.text=duplicate was deleted +dialog.deleteduplicates.multi.text=duplicates were deleted +dialog.deleteduplicates.nonefound=No duplicates found +dialog.compresstrack.title=Compress Track +dialog.compresstrack.parameter.text=Parameter for compression (lower number = more compression) +dialog.compresstrack.text=Track compressed +dialog.compresstrack.single.text=data point was removed +dialog.compresstrack.multi.text=data points were removed +dialog.compresstrack.nonefound=No data points could be removed +dialog.openoptions.title=Open options +dialog.openoptions.filesnippet=Extract of file +dialog.load.table.field=Field +dialog.load.table.datatype=Data Type +dialog.load.table.description=Description +dialog.delimiter.label=Field delimiter +dialog.delimiter.comma=Comma , +dialog.delimiter.tab=Tab +dialog.delimiter.space=Space +dialog.delimiter.semicolon=Semicolon ; +dialog.delimiter.other=Other +dialog.openoptions.deliminfo.records=records, with +dialog.openoptions.deliminfo.fields=fields +dialog.openoptions.deliminfo.norecords=No records +dialog.openoptions.tabledesc=Extract of file +dialog.openoptions.altitudeunits=Altitude units +dialog.saveoptions.title=Save file +dialog.save.fieldstosave=Fields to save +dialog.save.table.field=Field +dialog.save.table.hasdata=Has data +dialog.save.table.save=Save +dialog.save.coordinateunits=Coordinate units +dialog.save.units.original=Original +dialog.save.altitudeunits=Altitude units +dialog.save.oktitle=File saved +dialog.save.ok1=Successfully saved +dialog.save.ok2=points to file +dialog.exportkml.title=Export KML +dialog.exportkml.text=Please enter a short description for the data +dialog.confirmreversetrack.title=Confirm reversal +dialog.confirmreversetrack.text=This track contains timestamp information, which will be out of sequence after a reversal.\nAre you sure you want to reverse this section? +dialog.interpolate.title=Interpolate points +dialog.interpolate.parameter.text=Number of points to insert between selected points +dialog.undo.title=Undo action(s) +dialog.undo.pretext=Please select the action(s) to undo +dialog.confirmundo.title=Operation(s) undone +dialog.confirmundo.single.text=operation undone. +dialog.confirmundo.multiple.text=operations undone. +dialog.undo.none.title=Cannot undo +dialog.undo.none.text=No operations to undo! +dialog.clearundo.title=Clear undo list +dialog.clearundo.text=Are you sure you want to clear the undo list?\nAll undo information will be lost! +dialog.about.title=About +dialog.about.version=Version +dialog.about.build=Build +dialog.about.summarytext1=Prune is a program for loading, displaying and editing data from GPS receivers. +dialog.about.summarytext2=It is released under the Gnu GPL for free, open, worldwide use and enhancement.
Copying, redistribution and modification are permitted and encouraged
according to the conditions in the included license.txt file. +dialog.about.summarytext3=Please see http://activityworkshop.net/ for more information and user guides. + +# Buttons +button.ok=OK +button.back=Back +button.next=Next +button.finish=Finish +button.cancel=Cancel +button.moveup=Move up +button.movedown=Move down +button.startrange=Set range start +button.endrange=Set range end +button.deletepoint=Delete point +button.deleterange=Delete range +button.exit=Exit + +# Display components +display.nodata=No data loaded +display.noaltitudes=Track data does not include altitudes +details.trackdetails=Track details +details.notrack=No track loaded +details.track.points=Points +details.track.file=File +details.track.numfiles=Number of files +details.pointdetails=Point details +details.index.selected=Index +details.index.of=of +details.nopointselection=No point selected +details.norangeselection=No range selected +details.rangedetails=Range details +details.range.selected=Selected +details.range.to=to +details.altitude.to=to +details.range.climb=Climb +details.range.descent=Descent +details.distanceunits=Distance units +display.range.time.secs=s +display.range.time.mins=m +display.range.time.hours=h +display.range.time.days=d + +# Field names +fieldname.latitude=Latitude +fieldname.longitude=Longitude +fieldname.altitude=Altitude +fieldname.timestamp=Timestamp +fieldname.waypointname=Name +fieldname.waypointtype=Type +fieldname.newsegment=Segment +fieldname.custom=Custom +fieldname.prefix=Field +fieldname.distance=Distance +fieldname.duration=Duration + +# Measurement units +units.metres=Metres +units.metres.short=m +units.feet=Feet +units.feet.short=ft +units.kilometres=Kilometres +units.kilometres.short=km +units.miles=Miles +units.miles.short=mi +units.degminsec=Deg-min-sec +units.degmin=Deg-min +units.deg=Degrees + +# Undo operations +undo.load=load data +undo.deletepoint=delete point +undo.deleterange=delete range +undo.compress=compress track +undo.insert=insert points +undo.deleteduplicates=delete duplicates +undo.reverse=reverse range +undo.rearrangewaypoints=rearrange waypoints + +# Error messages +error.save.dialogtitle=Error saving data +error.save.nodata=No data to save +error.save.fileexists=File already exists. Just to be safe this program won't overwrite it. +error.save.failed=Failed to save the data to file: +error.load.dialogtitle=Error loading data +error.load.noread=Cannot read file +error.undofailed.title=Undo failed +error.undofailed.text=Failed to undo operation +error.function.noop.title=Function had no effect +error.rearrange.noop=Rearranging waypoints had no effect +error.function.notimplemented.title=Function not available +error.function.notimplemented=Sorry, this function has not yet been implemented. diff --git a/tim/prune/lang/prune-texts_de.properties b/tim/prune/lang/prune-texts_de.properties new file mode 100644 index 0000000..2acf4d1 --- /dev/null +++ b/tim/prune/lang/prune-texts_de.properties @@ -0,0 +1,187 @@ +# Text entries for the Prune application +# German entries as extra + +# Menu entries +menu.file=Datei +menu.file.open=Öffnen +menu.file.save=Speichern +menu.file.export=KML exportieren +menu.file.exit=Beenden +menu.edit=Bearbeiten +menu.edit.undo=Undo +menu.edit.clearundo=Undo-Liste löschen +menu.edit.deletepoint=Punkt löschen +menu.edit.deleterange=Spanne löschen +menu.edit.deleteduplicates=Duplikate löschen +menu.edit.compress=Track komprimieren +menu.edit.interpolate=Interpolieren +menu.edit.reverse=Spanne umkehren +menu.edit.rearrange=Waypoints reorganisieren +menu.edit.rearrange.start=Alle zum Anfang +menu.edit.rearrange.end=Alle zum Ende +menu.edit.rearrange.nearest=Jeder zum nächsten Trackpunkt +menu.select=Selektieren +menu.select.all=Alles selektieren +menu.select.none=Nichts selektieren +menu.3d=Drei-D +menu.3d.show3d=In drei-D zeigen +menu.help=Hilfe +menu.help.about=Über Prune +# Popup menu for map +menu.map.zoomin=Einzoomen +menu.map.zoomout=Auszoomen +menu.map.zoomfull=Zoomen zum ganzes Bild +menu.map.autopan=Autopan + +# Dialogs +dialog.exit.confirm.title=Prune beenden +dialog.exit.confirm.text=Ihre Daten wurden nicht gespeichert. Wollen Sie trotzdem das Programm beenden? +dialog.openappend.title=Daten anhängen oder ersetzen +dialog.openappend.text=Häng diese Daten zu den aktuellen Daten an? +dialog.deleteduplicates.title=Duplikate löschen +dialog.deleteduplicates.single.text=Duplikat wurde gelöscht +dialog.deleteduplicates.multi.text=Duplikate wurden gelöscht +dialog.deleteduplicates.nonefound=Keine Duplikate gefunden +dialog.compresstrack.title=Track komprimieren +dialog.compresstrack.parameter.text=Parameter für Komprimierung (niedriger Nummer = höher Komprimierung) +dialog.compresstrack.text=Track komprimiert +dialog.compresstrack.single.text=Punkt wurde entfernt +dialog.compresstrack.multi.text=Punkte wurden entfernt +dialog.compresstrack.nonefound=Keine Punkte konnten entfernt werden +dialog.openoptions.title=Öffnen Optionen +dialog.openoptions.filesnippet=Extrakt vom File +dialog.load.table.field=Feld +dialog.load.table.datatype=Daten Typ +dialog.load.table.description=Beschreibung +dialog.delimiter.label=Feld Trennzeichen +dialog.delimiter.comma=Komma , +dialog.delimiter.tab=Tab +dialog.delimiter.space=Abstand +dialog.delimiter.semicolon=Strichpunkt ; +dialog.delimiter.other=Andere +dialog.openoptions.deliminfo.records=Rekords, mit +dialog.openoptions.deliminfo.fields=Feldern +dialog.openoptions.deliminfo.norecords=Keine Rekords +dialog.openoptions.tabledesc=Extrakt vom File +dialog.openoptions.altitudeunits=Höhe Maßeinheiten +dialog.saveoptions.title=File speichern +dialog.save.fieldstosave=Felder zu speichern +dialog.save.table.field=Feld +dialog.save.table.hasdata=Hat Daten +dialog.save.table.save=Speichern +dialog.save.coordinateunits=Koordinaten Maßeinheiten +dialog.save.units.original=Original +dialog.save.altitudeunits=Höhe Maßeinheiten +dialog.save.oktitle=File gespeichert +dialog.save.ok1=Es wurden +dialog.save.ok2=Punkte gespeichert nach +dialog.exportkml.title=KML exportieren +dialog.exportkml.text=Kurze Beschreibung von den Daten +dialog.confirmreversetrack.title=Umkehrung bestätigen +dialog.confirmreversetrack.text=Diese Daten enthalten Zeitstempel Informationen, die bei einer Umkehrung ausser Reihenfolge erscheinen würden.\nSind Sie sicher, Sie wollen diese Spanne umkehren? +dialog.interpolate.title=Punkte interpolieren +dialog.interpolate.parameter.text=Anzahl Punkte zuzufügen zwischen den selektierten Punkten +dialog.undo.title=Undo Operation(en) +dialog.undo.pretext=Selektieren die Operationen die rückgängig gemacht werden sollen. +dialog.confirmundo.title=Operation(en) rückgängig gemacht +dialog.confirmundo.single.text=Operation rückgängig gemacht. +dialog.confirmundo.multiple.text=Operationen rückgängig gemacht. +dialog.undo.none.title=Undo nicht möglich +dialog.undo.none.text=Keine Operationen können rückgängig gemacht werden. +dialog.clearundo.title=Undo-Liste löschen +dialog.clearundo.text=Sind Sie sicher, Sie wollen die Undo-Liste löschen?\nAlle Undo Information wird veloren gehen! +dialog.about.title=Über +dialog.about.version=Version +dialog.about.build=Build +dialog.about.summarytext1=Prune ist ein Programm für das Laden, Darstellen und Editieren von Daten von GPS Geräten. +dialog.about.summarytext2=Es ist unter den Gnu GPL zur Verfügung gestellt, für frei, gratis und offen Gebrauch und Weiterentwicklung.
Kopieren, Weiterverbreitung und Veränderungen sind erlaubt und willkommen
unter die Bedingungen im enthaltenen license.txt File. +dialog.about.summarytext3=Bitte sehen Sie http://activityworkshop.net/ für weitere Information und Benutzeranleitungen. + +# Buttons +button.ok=OK +button.back=Zurück +button.next=Vorwärts +button.finish=Fertig +button.cancel=Abbrechen +button.moveup=Aufwärts moven +button.movedown=Abwärts moven +button.startrange=Start setzen +button.endrange=Stopp setzen +button.deletepoint=Punkt löschen +button.deleterange=Spanne löschen +button.exit=Beenden + +# Display components +display.nodata=Keine Daten geladen +display.noaltitudes=Track enthält keine Höhe Daten +details.trackdetails=Details vom Track +details.notrack=Kein Track geladen +details.track.points=Punkte +details.track.file=Datei +details.track.numfiles=Anzahl Dateien +details.pointdetails=Details vom Punkt +details.index.selected=Index +details.index.of=von +details.nopointselection=Nichts selektiert +details.norangeselection=Nichts selektiert +details.rangedetails=Details von Spanne +details.range.selected=Selektiert +details.range.to=bis +details.altitude.to=bis +details.range.climb=Aufstieg +details.range.descent=Abstieg +details.distanceunits=Distanz Maßeinheiten +display.range.time.secs=S +display.range.time.mins=M +display.range.time.hours=Std +display.range.time.days=T + +# Field names +fieldname.latitude=Breitengrad +fieldname.longitude=Längengrad +fieldname.altitude=Höhe +fieldname.timestamp=Zeitstempel +fieldname.waypointname=Name +fieldname.waypointtype=Typ +fieldname.newsegment=Segment +fieldname.custom=Custom +fieldname.prefix=Feld +fieldname.distance=Länge +fieldname.duration=Zeitlänge + +# Measurement units +units.metres=Meter +units.metres.short=M +units.feet=Füße +units.feet.short=F +units.kilometres=Kilometer +units.kilometres.short=Km +units.miles=Meilen +units.miles.short=Mei +units.degminsec=Grad-Min-Sek +units.degmin=Grad-Min +units.deg=Grad + +# Undo operations +undo.load=Daten laden +undo.deletepoint=Punkt löschen +undo.deleterange=Spanne löschen +undo.compress=Track komprimieren +undo.insert=Punkte dazufügen +undo.deleteduplicates=Duplikaten löschen +undo.reverse=Spanne umdrehen +undo.rearrangewaypoints=Waypoints reorganisieren + +# Error messages +error.save.dialogtitle=Fehler beim Speichern +error.save.nodata=Keine Daten wurden geladen +error.save.fileexists=File existiert schon. Um sicher zu sein, wird dieses Programm den File nicht überschreiben. +error.save.failed=Speichern vom File fehlgeschlagen : +error.load.dialogtitle=Fehler beim Laden +error.load.noread=File konnte nicht gelesen werden +error.undofailed.title=Undo fehlgeschlagen +error.undofailed.text=Operation konnte nicht rückgängig gemacht werden +error.function.noop.title=Funktion hat nichts gemacht +error.rearrange.noop=Waypoints Reorganisieren hatte keinen Effekt +error.function.notimplemented.title=Funktion nicht verfügbar +error.function.notimplemented=Sorry, diese Funktion wurde noch nicht implementiert. diff --git a/tim/prune/lang/prune-texts_de_CH.properties b/tim/prune/lang/prune-texts_de_CH.properties new file mode 100644 index 0000000..dd7bc9f --- /dev/null +++ b/tim/prune/lang/prune-texts_de_CH.properties @@ -0,0 +1,187 @@ +# Text entries for the Prune application +# Swiss-German entries as extra + +# Menu entries +menu.file=Datei +menu.file.open=Öffne +menu.file.save=Speichere +menu.file.export=KML exportiere +menu.file.exit=Beände +menu.edit=Editiere +menu.edit.undo=Undo +menu.edit.clearundo=Undo-Liste lösche +menu.edit.deletepoint=Punkt lösche +menu.edit.deleterange=Spanne lösche +menu.edit.deleteduplicates=Doppeldate lösche +menu.edit.compress=Date komprimiere +menu.edit.interpolate=Interpoliere +menu.edit.reverse=Spanne umkehre +menu.edit.rearrange=Waypoints reorganisiere +menu.edit.rearrange.start=Alli zum Aafang +menu.edit.rearrange.end=Alli zum Ände +menu.edit.rearrange.nearest=Jede zum nöchsti Trackpunkt +menu.select=Selektiere +menu.select.all=Alles selektiere +menu.select.none=Nüüt selektiere +menu.3d=Drüü-D +menu.3d.show3d=In drüü-D zeigä +menu.help=Hilfe +menu.help.about=Über Prune +# Popup menu for map +menu.map.zoomin=Einzoome +menu.map.zoomout=Uuszoome +menu.map.zoomfull=Zoome zum ganzes Bild +menu.map.autopan=Autopan + +# Dialogs +dialog.exit.confirm.title=Prune beände +dialog.exit.confirm.text=Ihri Date sind nonig gspeicheret worde. Wend Sie trotzdem s Programm beände? +dialog.openappend.title=Date aahänge oder ersätze +dialog.openappend.text=Häng diese Date zur aktuelli Daten aa? +dialog.deleteduplicates.title=Duplikaten lösche +dialog.deleteduplicates.single.text=Duplikat isch glöscht worde +dialog.deleteduplicates.multi.text=Duplikaten sin glöscht worde +dialog.deleteduplicates.nonefound=Keine Duplikaten gefunden +dialog.compresstrack.title=Track komprimiere +dialog.compresstrack.parameter.text=Parameter für Komprimierig (niedriger Nummer = höher Komprimierig) +dialog.compresstrack.text=Track komprimiert worde +dialog.compresstrack.single.text=Punkt isch entfernt worde +dialog.compresstrack.multi.text=Punkte sin entfernt worde +dialog.compresstrack.nonefound=Kei Punkte hätte gelöscht werde könne +dialog.openoptions.title=Öffne Optionen +dialog.openoptions.filesnippet=Extrakt vom File +dialog.load.table.field=Fäld +dialog.load.table.datatype=Date Typ +dialog.load.table.description=Beschriibig +dialog.delimiter.label=Fäld Trennzeiche +dialog.delimiter.comma=Komma , +dialog.delimiter.tab=Tab +dialog.delimiter.space=Abstand +dialog.delimiter.semicolon=Strichpunkt ; +dialog.delimiter.other=Andere +dialog.openoptions.deliminfo.records=Rekords, mit +dialog.openoptions.deliminfo.fields=Fäldere +dialog.openoptions.deliminfo.norecords=Kei Rekords +dialog.openoptions.tabledesc=Extrakt vom File +dialog.openoptions.altitudeunits=Höchi Massiiheite +dialog.saveoptions.title=File speichere +dialog.save.fieldstosave=Fälder zu speichere +dialog.save.table.field=Fäld +dialog.save.table.hasdata=Het Date +dialog.save.table.save=Speichere +dialog.save.coordinateunits=Koordinate Massiiheite +dialog.save.units.original=Original +dialog.save.altitudeunits=Höchi Massiiheite +dialog.save.oktitle=File gespeichert worde +dialog.save.ok1=Es isch +dialog.save.ok2=Punkte gespeichert worde na +dialog.exportkml.title=KML exportiere +dialog.exportkml.text=Kurze Beschriibig von den Date +dialog.confirmreversetrack.title=Umdrehig bestätige +dialog.confirmreversetrack.text=Diese Daten enthalte Ziitstämpel Informatione, die bei dr Umkehrig usser Reihefolge erschiene würdi.\nSind Sie sicher, Sie wend diese Spanne umkehre? +dialog.interpolate.title=Punkte interpoliere +dialog.interpolate.parameter.text=Aazahl Punkte zum innätue zwüschet den selektierten Punkten +dialog.undo.title=Undo Operation(e) +dialog.undo.pretext=Selektiere die Operatione die rückgängig gmacht söllti werde. +dialog.confirmundo.title=Operation(e) rückgängig gmacht worde +dialog.confirmundo.single.text=Operation rückgängig gmacht worde. +dialog.confirmundo.multiple.text=Operatione rückgängig gmacht worde. +dialog.undo.none.title=Undo nöd möglich +dialog.undo.none.text=Keini Operatione könne rückgängig gmacht werde. +dialog.clearundo.title=Undo-Liste lösche +dialog.clearundo.text=Sind Sie sicher, Sie wend die Undo-Liste lösche?\nAlle Undo Infos werdet verlore gah! +dialog.about.title=Über +dialog.about.version=Version +dialog.about.build=Build +dialog.about.summarytext1=Prune isch n Programm fürs Lade, Darstelle und Editiere vo Date von GPS Geräte. +dialog.about.summarytext2=Es isch unter den Gnu GPL zur Verfüegig gstellt,für frei, gratis und offen Gebruuch und Wiiterentwicklig.
Kopiere, Wiiterverbreitig und Veränderige sin erlaubt und willkommen
unter die Bedingunge im enthaltene license.txt File. +dialog.about.summarytext3=Bitte lueg na http://activityworkshop.net/ für wiitere Information und Benutzeraaleitige. + +# Buttons +button.ok=OK +button.back=Zrugg +button.next=Nöchste +button.finish=Fertig +button.cancel=Abbräche +button.moveup=Uufwärts move +button.movedown=Abwärts move +button.startrange=Start setze +button.endrange=Stopp setze +button.deletepoint=Punkt lösche +button.deleterange=Spanne lösche +button.exit=Beände + +# Display components +display.nodata=Kei Date glade worde +display.noaltitudes=Track hät kei Höhi Date +details.trackdetails=Details vom Track +details.notrack=Kei Track glade worde +details.track.points=Punkte +details.track.file=Datei +details.track.numfiles=Anzahl Dateie +details.pointdetails=Details vom Punkt +details.index.selected=Index +details.index.of=vo +details.nopointselection=Nüüt selektiert +details.norangeselection=Nüüt selektiert +details.rangedetails=Details vo dr Spanne +details.range.selected=Selektiert +details.range.to=bis +details.altitude.to=bis +details.range.climb=Uufstieg +details.range.descent=Abstieg +details.distanceunits=Distanz Masseinheiten +display.range.time.secs=S +display.range.time.mins=M +display.range.time.hours=Std +display.range.time.days=T + +# Field names +fieldname.latitude=Breitegrad +fieldname.longitude=Längegrad +fieldname.altitude=Höchi +fieldname.timestamp=Ziitstämpel +fieldname.waypointname=Name +fieldname.waypointtype=Typ +fieldname.newsegment=Segmänt +fieldname.custom=Custom +fieldname.prefix=Fäld +fieldname.distance=Längi +fieldname.duration=Ziitlängi + +# Measurement units +units.metres=Meter +units.metres.short=M +units.feet=Fuess +units.feet.short=F +units.kilometres=Kilometer +units.kilometres.short=Km +units.miles=Meile +units.miles.short=Mei +units.degminsec=Grad-Min-Sek +units.degmin=Grad-Min +units.deg=Grad + +# Undo operations +undo.load=Date lade +undo.deletepoint=Punkt lösche +undo.deleterange=Spanne lösche +undo.compress=Track komprimiere +undo.insert=Punkte innätue +undo.deleteduplicates=Duplikaten lösche +undo.reverse=Spanne umdrähie +undo.rearrangewaypoints=Waypoints reorganisiere + +# Error messages +error.save.dialogtitle=Fehler bim Speichere +error.save.nodata=Kei Date zum speichere +error.save.fileexists=File existiert scho. Um sicher z'sii, wird s'Programm s'File nöd überschriibe. +error.save.failed=Speichere vom File fehlgschlage : +error.load.dialogtitle=Fehler bim Lade +error.load.noread=File cha nöd glase werde +error.undofailed.title=Undo isch fehlgschlage +error.undofailed.text=Operation kann nöd rückgängig gmacht werde +error.function.noop.title=Funktion hät gar nüüt gmacht +error.rearrange.noop=Waypoints Reorganisiere hät kein Effäkt gha +error.function.notimplemented.title=Funktion nöd verfüegbar +error.function.notimplemented=Sorry, d'Funktion isch nonig implementiert worde. diff --git a/tim/prune/license.txt b/tim/prune/license.txt new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/tim/prune/license.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/tim/prune/load/DelimiterInfo.java b/tim/prune/load/DelimiterInfo.java new file mode 100644 index 0000000..d882972 --- /dev/null +++ b/tim/prune/load/DelimiterInfo.java @@ -0,0 +1,63 @@ +package tim.prune.load; + +/** + * Class to hold information about the contents of a file + * given a delimiter character + */ +public class DelimiterInfo +{ + private char _delimiter = '\0'; + private int _numRecords = 0; + private int _numWinningRecords = 0; + private int _maxFields = 0; + + + /** + * Constructor + * @param inChar delimiter character + */ + public DelimiterInfo(char inChar) + { + _delimiter = inChar; + } + + public char getDelimiter() + { + return _delimiter; + } + + public int getMaxFields() + { + return _maxFields; + } + + public void updateMaxFields(int inNumields) + { + if (inNumields > _maxFields) + _maxFields = inNumields; + } + + + public int getNumRecords() + { + return _numRecords; + } + public void incrementNumRecords() + { + _numRecords++; + } + + public int getNumWinningRecords() + { + return _numWinningRecords; + } + public void incrementNumWinningRecords() + { + _numWinningRecords++; + } + + public String toString() + { + return "(delim:" + _delimiter + " fields:" + _maxFields + ", records:" + _numRecords + ")"; + } +} diff --git a/tim/prune/load/FieldSelectionTableModel.java b/tim/prune/load/FieldSelectionTableModel.java new file mode 100644 index 0000000..11b60d9 --- /dev/null +++ b/tim/prune/load/FieldSelectionTableModel.java @@ -0,0 +1,240 @@ +package tim.prune.load; + +import javax.swing.table.AbstractTableModel; + +import tim.prune.I18nManager; +import tim.prune.data.Field; + +/** + * Class to hold the table model for the field selection table + */ +public class FieldSelectionTableModel extends AbstractTableModel +{ + + private int _numRows = 0; + private Field[] _fieldArray = null; + private String _customText = null; + + /** + * Constructor + */ + public FieldSelectionTableModel() + { + // Cache the custom text for the table so it doesn't + // have to be looked up so often + _customText = I18nManager.getText("fieldname.custom"); + } + + + /** + * Get the column count + */ + public int getColumnCount() + { + return 3; + } + + + /** + * Get the name of the column + */ + public String getColumnName(int inColNum) + { + if (inColNum == 0) return I18nManager.getText("dialog.load.table.field"); + else if (inColNum == 1) return I18nManager.getText("dialog.load.table.datatype"); + return I18nManager.getText("dialog.load.table.description"); + } + + + /** + * Get the row count + */ + public int getRowCount() + { + if (_fieldArray == null) + return 2; + return _numRows; + } + + + /** + * Get the value of the specified cell + */ + public Object getValueAt(int rowIndex, int columnIndex) + { + if (_fieldArray == null) return ""; + if (columnIndex == 0) return ("" + (rowIndex+1)); + Field field = _fieldArray[rowIndex]; + if (columnIndex == 1) + { + // Field name - take name from built-in fields + if (field.isBuiltIn()) + return field.getName(); + // Otherwise take custom name + return _customText; + } + // description column - builtin fields don't have one + if (field.isBuiltIn()) return ""; + return field.getName(); + } + + + /** + * Make sure only second and third columns are editable + */ + public boolean isCellEditable(int rowIndex, int columnIndex) + { + if (columnIndex <= 1) + return (columnIndex == 1); + // Column is 2 so only edit non-builtin field names + Field field = _fieldArray[rowIndex]; + return !field.isBuiltIn(); + } + + + /** + * Update the data + * @param inData 2-dimensional Object array containing the data + */ + public void updateData(Field[] inData) + { + _fieldArray = inData; + if (_fieldArray != null) + { + _numRows = _fieldArray.length; + } + fireTableStructureChanged(); + } + + + /** + * React to edits to the table data + */ + public void setValueAt(Object aValue, int rowIndex, int columnIndex) + { + super.setValueAt(aValue, rowIndex, columnIndex); + if (columnIndex == 1) + { + Field field = _fieldArray[rowIndex]; + if (!field.getName().equals(aValue.toString())) + { + manageFieldChange(rowIndex, aValue.toString()); + } + } + else if (columnIndex == 2) + { + // change description if it's custom + Field field = _fieldArray[rowIndex]; + if (!field.isBuiltIn()) + field.setName(aValue.toString()); + } + } + + + /** + * Move the selected item up one place + * @param inIndex index of item to move + */ + public void moveUp(int inIndex) + { + if (inIndex > 0) + { + swapItems(inIndex-1, inIndex); + } + } + + + /** + * Move the selected item down one place + * @param inIndex index of item to move + */ + public void moveDown(int inIndex) + { + if (inIndex > -1 && inIndex < (_numRows - 1)) + { + swapItems(inIndex, inIndex+1); + } + } + + + /** + * Swap the specified items in the array + * @param inIndex1 index of first item + * @param inIndex2 index of second item (higher than inIndex1) + */ + private void swapItems(int inIndex1, int inIndex2) + { + Field temp = _fieldArray[inIndex1]; + _fieldArray[inIndex1] = _fieldArray[inIndex2]; + _fieldArray[inIndex2] = temp; + fireTableRowsUpdated(inIndex1, inIndex2); + } + + + /** + * React to a requested change to one of the fields + * @param inRow row number of change + * @param inValue new string value + */ + private void manageFieldChange(int inRow, String inValue) + { + // check if it's lat or long - don't allow changes to these fields + Field field = _fieldArray[inRow]; + if (field == Field.LATITUDE || field == Field.LONGITUDE) + return; + if (inValue.equals(I18nManager.getText("fieldname.latitude")) + || inValue.equals(I18nManager.getText("fieldname.longitude"))) + return; + + // Changes to custom field need to be handled differently + boolean changeToCustom = inValue.equals(I18nManager.getText("fieldname.custom")); + if (changeToCustom) + { + if (field.isBuiltIn()) + { + String customPrefix = I18nManager.getText("fieldname.prefix") + " "; + int index = inRow + 1; + while (hasField(customPrefix + index)) + index++; + _fieldArray[inRow] = new Field(customPrefix + index); + } + // ignore custom to custom changes + } + else + { + // Change to a fixed field - check we've not already got it + if (!hasField(inValue)) + { + // Change is ok - find new Field object corresponding to text + for (int i=0; i 0) + contentList.add(currLine); + currLine = reader.readLine(); + } + } + catch (IOException ioe) {} + finally + { + // close file ignoring errors + try + { + if (reader != null) reader.close(); + } + catch (Exception e) {} + } + } + // Convert into String array for keeps + int numLines = contentList.size(); + _contentArray = new String[numLines]; + for (int i=0; i getNumLines()) numToCopy = getNumLines(); + int size = numToCopy; + if (size < MIN_SNIPPET_SIZE) size = MIN_SNIPPET_SIZE; + String[] result = new String[size]; + // Copy Strings across + System.arraycopy(_contentArray, 0, result, 0, numToCopy); + // Chop Strings to max width if necessary + if (inMaxWidth > 10) + { + for (int i=0; i inMaxWidth) + result[i] = result[i].trim(); + if (result[i].length() > inMaxWidth) + result[i] = result[i].substring(0, inMaxWidth); + } + } + return result; + } + + /** + * @return the number of non-blank lines in the file + */ + public int getNumLines() + { + return _contentArray.length; + } + + + /** + * Clear the memory + */ + public void clear() + { + _file = null; + _contentArray = null; + } +} diff --git a/tim/prune/load/FileExtractTableModel.java b/tim/prune/load/FileExtractTableModel.java new file mode 100644 index 0000000..42f39b3 --- /dev/null +++ b/tim/prune/load/FileExtractTableModel.java @@ -0,0 +1,81 @@ +package tim.prune.load; + +import javax.swing.table.AbstractTableModel; + +/** + * Class to hold the table model for the file extract table + */ +public class FileExtractTableModel extends AbstractTableModel +{ + + private int _numRows = 0; + private Object[][] _tableData = null; + + /** + * Get the column count + */ + public int getColumnCount() + { + if (_tableData == null) + return 2; + return _tableData[0].length; + } + + /** + * Get the name of the column, in this case just the number + */ + public String getColumnName(int inColNum) + { + return "" + (inColNum + 1); + } + + /** + * Get the row count + */ + public int getRowCount() + { + if (_tableData == null) + return 2; + return _numRows; + } + + /** + * Get the value of the specified cell + */ + public Object getValueAt(int rowIndex, int columnIndex) + { + if (_tableData == null) return ""; + return _tableData[rowIndex][columnIndex]; + } + + /** + * Make sure table data is not editable + */ + public boolean isCellEditable(int rowIndex, int columnIndex) + { + return false; + } + + /** + * Update the data + * @param inData 2-dimensional Object array containing the data + */ + public void updateData(Object[][] inData) + { + _tableData = inData; + if (_tableData != null) + { + _numRows = _tableData.length; + } + fireTableStructureChanged(); + } + + + /** + * @return Object array of data + */ + public Object[][] getData() + { + return _tableData; + } +} diff --git a/tim/prune/load/FileLoader.java b/tim/prune/load/FileLoader.java new file mode 100644 index 0000000..c7126f5 --- /dev/null +++ b/tim/prune/load/FileLoader.java @@ -0,0 +1,563 @@ +package tim.prune.load; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellEditor; + +import java.io.File; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.Field; + + +/** + * Special class to handle file loading including GUI options, + * and conversion to a Track object + */ +public class FileLoader +{ + private File _file = null; + private App _app = null; + private JFrame _parentFrame = null; + private JDialog _dialog = null; + private JPanel _cardPanel = null; + private CardLayout _layout = null; + private JButton _backButton = null, _nextButton = null; + private JButton _finishButton = null; + private JButton _moveUpButton = null, _moveDownButton = null; + private JRadioButton[] _delimiterRadios = null; + private JTextField _otherDelimiterText = null; + private JLabel _statusLabel = null; + private DelimiterInfo[] _delimiterInfos = null; + private JFileChooser _fileChooser = null; + private FileCacher _fileCacher = null; + private JList _snippetBox = null; + private FileExtractTableModel _fileExtractTableModel = null; + private JTable _fieldTable; + private FieldSelectionTableModel _fieldTableModel = null; + private JComboBox _unitsDropDown = null; + private int _selectedField = -1; + private char _currentDelimiter = ','; + + // previously selected values + private char _lastUsedDelimiter = ','; + private int _lastNumFields = -1; + private Field[] _lastSelectedFields = null; + private int _lastAltitudeFormat = Altitude.FORMAT_NONE; + + // constants + private static final int SNIPPET_SIZE = 6; + private static final int MAX_SNIPPET_WIDTH = 80; + private static final char[] DELIMITERS = {',', '\t', ';', ' '}; + + + /** + * Inner class to listen for delimiter change operations + */ + private class DelimListener implements ActionListener, DocumentListener + { + public void actionPerformed(ActionEvent e) + { + informDelimiterSelected(); + } + public void changedUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + public void insertUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + public void removeUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + } + + + /** + * Constructor + * @param inApp Application object to inform of track load + * @param inParentFrame parent frame to reference for dialogs + */ + public FileLoader(App inApp, JFrame inParentFrame) + { + _app = inApp; + _parentFrame = inParentFrame; + } + + + /** + * Select an input file and open the GUI frame + * to select load options + */ + public void openFile() + { + if (_fileChooser == null) + _fileChooser = new JFileChooser(); + if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + _file = _fileChooser.getSelectedFile(); + if (preCheckFile(_file)) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + + // select best separator according to row counts (more is better) + int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(), + _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(), + _delimiterInfos[3].getNumWinningRecords()); + if (bestDelim >= 0) + _delimiterRadios[bestDelim].setSelected(true); + else + _delimiterRadios[_delimiterRadios.length-1].setSelected(true); + informDelimiterSelected(); + _dialog.pack(); + _dialog.show(); + } + else + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"), + I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + } + } + + + /** + * Check the given file for readability and funny characters, + * and count the fields for the various separators + * @param inFile file to check + */ + private boolean preCheckFile(File inFile) + { + // Check file exists and is readable + if (inFile == null || !inFile.exists() || !inFile.canRead()) + { + return false; + } + // Use a FileCacher to read the file into an array + _fileCacher = new FileCacher(inFile); + + // Check each line of the file + String[] fileContents = _fileCacher.getContents(); + boolean fileOK = true; + _delimiterInfos = new DelimiterInfo[5]; + for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]); + + String currLine = null; + String[] splitFields = null; + int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0; + for (int lineNum=0; lineNum= 0) {fileOK = false;} + // check for commas + splitFields = currLine.split(","); + commaFields = splitFields.length; + if (commaFields > 1) _delimiterInfos[0].incrementNumRecords(); + _delimiterInfos[0].updateMaxFields(commaFields); + // check for tabs + splitFields = currLine.split("\t"); + tabFields = splitFields.length; + if (tabFields > 1) _delimiterInfos[1].incrementNumRecords(); + _delimiterInfos[1].updateMaxFields(tabFields); + // check for semicolons + splitFields = currLine.split(";"); + semicolonFields = splitFields.length; + if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords(); + _delimiterInfos[2].updateMaxFields(semicolonFields); + // check for spaces + splitFields = currLine.split(" "); + spaceFields = splitFields.length; + if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords(); + _delimiterInfos[3].updateMaxFields(spaceFields); + // increment counters + int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields); + if (bestScorer >= 0) + _delimiterInfos[bestScorer].incrementNumWinningRecords(); + } + return fileOK; + } + + + /** + * Get the index of the best one in the list + * @return the index of the maximum of the four given values + */ + private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3) + { + int bestIndex = -1; + int maxScore = 1; + if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;} + if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;} + if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;} + if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;} + return bestIndex; + } + + + /** + * Make the components for the open options dialog + * @return Component for all options + */ + private Component makeDialogComponents() + { + JPanel wholePanel = new JPanel(); + wholePanel.setLayout(new BorderLayout()); + + // add buttons to south + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); + _backButton = new JButton(I18nManager.getText("button.back")); + _backButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _layout.previous(_cardPanel); + _backButton.setEnabled(false); + _nextButton.setEnabled(true); + _finishButton.setEnabled(false); + } + }); + _backButton.setEnabled(false); + buttonPanel.add(_backButton); + _nextButton = new JButton(I18nManager.getText("button.next")); + _nextButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + prepareSecondPanel(); + _layout.next(_cardPanel); + _nextButton.setEnabled(false); + _backButton.setEnabled(true); + _finishButton.setEnabled(true); + } + }); + buttonPanel.add(_nextButton); + _finishButton = new JButton(I18nManager.getText("button.finish")); + _finishButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + finished(); + } + }); + _finishButton.setEnabled(false); + buttonPanel.add(_finishButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + wholePanel.add(buttonPanel, BorderLayout.SOUTH); + + // Make the two cards, for delimiter and fields + _cardPanel = new JPanel(); + _layout = new CardLayout(); + _cardPanel.setLayout(_layout); + JPanel firstCard = new JPanel(); + firstCard.setLayout(new BorderLayout()); + + JPanel delimsPanel = new JPanel(); + delimsPanel.setLayout(new GridLayout(0, 2)); + delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label"))); + delimsPanel.add(new JLabel("")); // blank label to go to next grid row + // radio buttons + _delimiterRadios = new JRadioButton[5]; + _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma")); + delimsPanel.add(_delimiterRadios[0]); + _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab")); + delimsPanel.add(_delimiterRadios[1]); + _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon")); + delimsPanel.add(_delimiterRadios[2]); + _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space")); + delimsPanel.add(_delimiterRadios[3]); + JPanel otherPanel = new JPanel(); + otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other")); + otherPanel.add(_delimiterRadios[4]); + _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2); + otherPanel.add(_otherDelimiterText); + // Group radio buttons + ButtonGroup delimGroup = new ButtonGroup(); + DelimListener delimListener = new DelimListener(); + for (int i=0; i<_delimiterRadios.length; i++) + { + delimGroup.add(_delimiterRadios[i]); + _delimiterRadios[i].addActionListener(delimListener); + } + _otherDelimiterText.getDocument().addDocumentListener(delimListener); + delimsPanel.add(new JLabel("")); + delimsPanel.add(otherPanel); + _statusLabel = new JLabel(""); + delimsPanel.add(_statusLabel); + firstCard.add(delimsPanel, BorderLayout.SOUTH); + // load snippet to show first few lines + _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH)); + _snippetBox.setEnabled(false); + firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER); + + // Second screen, for field order selection + JPanel secondCard = new JPanel(); + secondCard.setLayout(new BorderLayout()); + // table for file contents + _fileExtractTableModel = new FileExtractTableModel(); + JTable extractTable = new JTable(_fileExtractTableModel); + JScrollPane tableScrollPane = new JScrollPane(extractTable); + extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80)); + extractTable.getTableHeader().setReorderingAllowed(false); + secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH); + JPanel innerPanel2 = new JPanel(); + innerPanel2.setLayout(new BorderLayout()); + _fieldTable = new JTable(new FieldSelectionTableModel()); + _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + // add listener for selected table row + _fieldTable.getSelectionModel().addListSelectionListener( + new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + ListSelectionModel lsm = (ListSelectionModel) e.getSource(); + if (lsm.isSelectionEmpty()) { + //no rows are selected + selectField(-1); + } else { + selectField(lsm.getMinSelectionIndex()); + } + } + }); + JPanel tablePanel = new JPanel(); + tablePanel.setLayout(new BorderLayout()); + tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH); + tablePanel.add(_fieldTable, BorderLayout.CENTER); + innerPanel2.add(tablePanel, BorderLayout.CENTER); + + JPanel innerPanel3 = new JPanel(); + innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS)); + _moveUpButton = new JButton(I18nManager.getText("button.moveup")); + _moveUpButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + int currRow = _fieldTable.getSelectedRow(); + closeTableComboBox(currRow); + _fieldTableModel.moveUp(currRow); + _fieldTable.setRowSelectionInterval(currRow-1, currRow-1); + } + }); + innerPanel3.add(_moveUpButton); + _moveDownButton = new JButton(I18nManager.getText("button.movedown")); + _moveDownButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + int currRow = _fieldTable.getSelectedRow(); + closeTableComboBox(currRow); + _fieldTableModel.moveDown(currRow); + _fieldTable.setRowSelectionInterval(currRow+1, currRow+1); + } + }); + innerPanel3.add(_moveDownButton); + innerPanel3.add(Box.createVerticalStrut(70)); + + innerPanel2.add(innerPanel3, BorderLayout.EAST); + secondCard.add(innerPanel2, BorderLayout.CENTER); + JPanel altUnitsPanel = new JPanel(); + altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits"))); + String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")}; + _unitsDropDown = new JComboBox(units); + altUnitsPanel.add(_unitsDropDown); + secondCard.add(altUnitsPanel, BorderLayout.SOUTH); + _cardPanel.add(firstCard, "card1"); + _cardPanel.add(secondCard, "card2"); + + wholePanel.add(_cardPanel, BorderLayout.CENTER); + return wholePanel; + } + + + /** + * Close the combo box on the selected row of the field table + * @param inRow currently selected row number + */ + private void closeTableComboBox(int inRow) + { + TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1); + if (editor != null) + { + editor.stopCellEditing(); + } + } + + + /** + * change the status based on selection of a delimiter + */ + protected void informDelimiterSelected() + { + for (int i=0; i<(_delimiterRadios.length-1); i++) + { + if (_delimiterRadios[i].isSelected()) + { + int numRecords = _delimiterInfos[i].getNumRecords(); + if (numRecords == 0) + { + _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords")); + } + else + { + _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records") + + _delimiterInfos[i].getMaxFields() + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields")); + } + } + } + if (_delimiterRadios[_delimiterRadios.length-1].isSelected()) + { + _statusLabel.setText(""); + } + // enable/disable next button + _nextButton.setEnabled(_delimiterRadios[4].isSelected() == false + || _otherDelimiterText.getText().length() == 1); + } + + + /** + * Get the delimiter info from the first step + * @return delimiter information object for the selected delimiter + */ + public DelimiterInfo getSelectedDelimiterInfo() + { + for (int i=0; i<4; i++) + if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i]; + // must be "other" - build info if necessary + if (_delimiterInfos[4] == null) + _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0)); + return _delimiterInfos[4]; + } + + + /** + * Use the delimiter selected to determine the fields in the file + * and prepare the second panel accordingly + */ + private void prepareSecondPanel() + { + DelimiterInfo info = getSelectedDelimiterInfo(); + FileSplitter splitter = new FileSplitter(_fileCacher); + // Check info makes sense - num fields > 0, num records > 0 + // set "Finished" button to disabled if not ok + // TODO: Work out if there are header rows or not, save? + // Try to match header rows with fields + // Try to match data with fields + // Add data to GUI elements + Object[][] tableData = splitter.splitFieldData(info.getDelimiter()); + // possible to ignore blank columns here + _currentDelimiter = info.getDelimiter(); + _fileExtractTableModel.updateData(tableData); + _fieldTableModel = new FieldSelectionTableModel(); + + // Check number of fields and use last ones if count matches + Field[] startFieldArray = null; + if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length) + startFieldArray = _lastSelectedFields; + else + startFieldArray = splitter.makeDefaultFields(); + _fieldTableModel.updateData(startFieldArray); + _fieldTable.setModel(_fieldTableModel); + // add dropdowns to second column + JComboBox fieldTypesBox = new JComboBox(); + for (int i=0; i 0); + _moveDownButton.setEnabled(inFieldNum >= 0 + && inFieldNum < (_fieldTableModel.getRowCount()-1)); + } + } + + + /** + * @return the last delimiter character used for a load + */ + public char getLastUsedDelimiter() + { + return _lastUsedDelimiter; + } +} diff --git a/tim/prune/load/FileSplitter.java b/tim/prune/load/FileSplitter.java new file mode 100644 index 0000000..216e027 --- /dev/null +++ b/tim/prune/load/FileSplitter.java @@ -0,0 +1,147 @@ +package tim.prune.load; + +import tim.prune.I18nManager; +import tim.prune.data.Field; + +/** + * Class responsible for splitting the file contents into an array + * based on the selected delimiter character + */ +public class FileSplitter +{ + private FileCacher _cacher = null; + private int _numRows = 0; + private int _numColumns = 0; + private boolean[] _columnStates = null; + + /** + * Constructor + * @param inCacher FileCacher object holding file contents + */ + public FileSplitter(FileCacher inCacher) + { + _cacher = inCacher; + } + + /** + * Split the FileCacher's contents into a 2d array + * @param inDelim delimiter character + * @return 2d Object array + */ + public String[][] splitFieldData(char inDelim) + { + if (_cacher == null) return null; + String[] contents = _cacher.getContents(); + if (contents == null || contents.length == 0) return null; + String delimStr = "" + inDelim; + // Count non-blank rows and max field count + _numRows = 0; + int maxFields = 0; + for (int i=0; i maxFields) + { + maxFields = splitLine.length; + } + } + } + _numColumns = maxFields; + _columnStates = new boolean[maxFields]; + + // Create array and populate it + // Note that array will be rectangular even if data is ragged + String[][] result = new String[_numRows][]; + for (int i=0; i 0) + { + _columnStates[j] = true; + } + } + } + } + } + } + return result; + } + + + /** + * @return the number of rows in the data + */ + public int getNumRows() + { + return _numRows; + } + + + /** + * @return the number of columns in the data + */ + public int getNumColumns() + { + return _numColumns; + } + + + /** + * Check if the specified column of the data is blank + * @param inColumnNum number of column, starting with 0 + * @return true if no data exists in this column + */ + public boolean isColumnBlank(int inColumnNum) + { + // Should probably trap out of range values + return !_columnStates[inColumnNum]; + } + + + /** + * @return a Field array to use as defaults for the data + */ + public Field[] makeDefaultFields() + { + Field[] fields = null; + if (_numColumns > 0) + { + fields = new Field[_numColumns]; + try + { + fields[0] = Field.LATITUDE; + fields[1] = Field.LONGITUDE; + fields[2] = Field.ALTITUDE; + fields[3] = Field.WAYPT_NAME; + fields[4] = Field.WAYPT_TYPE; + String customPrefix = I18nManager.getText("fieldname.prefix") + " "; + for (int i=5;; i++) + { + fields[i] = new Field(customPrefix + (i+1)); + } + } + catch (ArrayIndexOutOfBoundsException finished) + { + // Finished populating array + } + } + else + fields = new Field[0]; + return fields; + } +} diff --git a/tim/prune/load/OneCharDocument.java b/tim/prune/load/OneCharDocument.java new file mode 100644 index 0000000..9868984 --- /dev/null +++ b/tim/prune/load/OneCharDocument.java @@ -0,0 +1,21 @@ +package tim.prune.load; + +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; + +/** + * Document class to restrict text input to single character + * for selection of custom delimiter + */ +public class OneCharDocument extends PlainDocument +{ + public void insertString(int offs, String str, AttributeSet a) + throws BadLocationException + { + //This rejects the insertion if it would make + //the contents too long. + if ((getLength() + str.length()) <= 1) + super.insertString(offs, str, a); + } +} diff --git a/tim/prune/readme.txt b/tim/prune/readme.txt new file mode 100644 index 0000000..fc788d2 --- /dev/null +++ b/tim/prune/readme.txt @@ -0,0 +1,37 @@ +Prune version 1 +=============== + +Prune is an application for viewing, editing and managing coordinate data from GPS systems. + +Prune is copyright activityworkshop.net and distributed under the terms of the Gnu GPL version 2. +You may freely use the software, and may help others to freely use it too. For further information +on your rights and how they are protected, see the included license.txt file. + +Prune comes without warranty and without guarantee - the authors cannot be held responsible for +losses incurred through use of the program, however caused. + + +Running +======= + +To run Prune from the jar file, simply call it from a Command Prompt or shell: + java -jar prune_1.jar + +If the jar file is saved in a different directory, you will need to include the path. +Depending on your system settings, you may be able to click or double-click on the jar file +in a file manager window to execute it. A shortcut, menu item, desktop icon or other link +can of course be made should you wish. + + +Further information and updates +=============================== + +To obtain the source code (if it wasn't included in your jar file), or for further information, +please visit the website: http://activityworkshop.net/ + +You will find there user guides and screenshots illustrating the major features. +As Prune is further developed, subsequent versions of the program will also be made freely +available at this website. + +You can also provide feedback on Prune, and find out more about contributing to the development, +especially with regard to language translations. diff --git a/tim/prune/save/FieldInfo.java b/tim/prune/save/FieldInfo.java new file mode 100644 index 0000000..4098547 --- /dev/null +++ b/tim/prune/save/FieldInfo.java @@ -0,0 +1,69 @@ +package tim.prune.save; + +import tim.prune.data.Field; + +/** + * Class to hold field information for save dialog + */ +public class FieldInfo +{ + private Field _field = null; + private boolean _data = false; + private boolean _selected = true; + + + /** + * Constructor + */ + public FieldInfo(Field inField, boolean inData) + { + _field = inField; + _selected = _data = inData; + } + + + /** + * @return the field object + */ + public Field getField() + { + return _field; + } + + + /** + * @return true if field has data + */ + public boolean hasData() + { + return _data; + } + + + /** + * @return true if field is selected + */ + public boolean isSelected() + { + return _selected; + } + + + /** + * Set whether the field is selected or not + * @param inSelected true to select field + */ + public void setSelected(boolean inSelected) + { + _selected = inSelected; + } + + + /** + * @return String for debug + */ + public String toString() + { + return _field.getName() + (_data?"(data)":"(no data)") + ", " + (_selected?"(sel)":"(---)"); + } +} diff --git a/tim/prune/save/FieldSelectionTableModel.java b/tim/prune/save/FieldSelectionTableModel.java new file mode 100644 index 0000000..92fda1e --- /dev/null +++ b/tim/prune/save/FieldSelectionTableModel.java @@ -0,0 +1,142 @@ +package tim.prune.save; + +import javax.swing.table.AbstractTableModel; + +import tim.prune.I18nManager; + +/** + * Class to hold table model information for save dialog + */ +public class FieldSelectionTableModel extends AbstractTableModel +{ + private FieldInfo[] _info = null; + + + /** + * Constructor giving list size + */ + public FieldSelectionTableModel(int inSize) + { + _info = new FieldInfo[inSize]; + } + + + /** + * Set the given FieldInfo object in the array + * @param inInfo FieldInfo object describing the field + * @param inIndex index to place in array + */ + public void addFieldInfo(FieldInfo inInfo, int inIndex) + { + _info[inIndex] = inInfo; + } + + + /** + * @see javax.swing.table.TableModel#getColumnCount() + */ + public int getColumnCount() + { + return 3; + } + + + /** + * @see javax.swing.table.TableModel#getRowCount() + */ + public int getRowCount() + { + return _info.length; + } + + + /** + * @see javax.swing.table.TableModel#getValueAt(int, int) + */ + public Object getValueAt(int inRowIndex, int inColumnIndex) + { + if (inColumnIndex == 0) + { + return _info[inRowIndex].getField().getName(); + } + else if (inColumnIndex == 1) + { + return new Boolean(_info[inRowIndex].hasData()); + } + return new Boolean(_info[inRowIndex].isSelected()); + } + + + /** + * @return true if cell is editable + */ + public boolean isCellEditable(int inRowIndex, int inColumnIndex) + { + // only the select column is editable + return inColumnIndex == 2; + } + + + /** + * Set the given cell value + * @see javax.swing.table.TableModel#setValueAt(java.lang.Object, int, int) + */ + public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex) + { + // ignore edits to other columns + if (inColumnIndex == 2) + _info[inRowIndex].setSelected(((Boolean) inValue).booleanValue()); + } + + + /** + * Swap the specified items in the array + * @param inIndex1 first index + * @param inIndex2 second index + */ + public void swapItems(int inIndex1, int inIndex2) + { + if (inIndex1 >= 0 && inIndex1 < _info.length && inIndex2 >= 0 && inIndex2 < _info.length) + { + FieldInfo temp = _info[inIndex1]; + _info[inIndex1] = _info[inIndex2]; + _info[inIndex2] = temp; + } + } + + + /** + * @return Class of cell data + */ + public Class getColumnClass(int inColumnIndex) + { + if (inColumnIndex==0) return String.class; + return Boolean.class; + } + + + /** + * Get the name of the column + */ + public String getColumnName(int inColNum) + { + if (inColNum == 0) return I18nManager.getText("dialog.save.table.field"); + else if (inColNum == 1) return I18nManager.getText("dialog.save.table.hasdata"); + return I18nManager.getText("dialog.save.table.save"); + } + + + /** + * Retrieve the FieldInfo object at the given index + * @param inIndex index, starting at 0 + * @return FieldInfo object at this position + */ + public FieldInfo getFieldInfo(int inIndex) + { + if (inIndex < 0 || inIndex >= _info.length) + { + return null; + } + return _info[inIndex]; + } +} diff --git a/tim/prune/save/FileSaver.java b/tim/prune/save/FileSaver.java new file mode 100644 index 0000000..b1ea368 --- /dev/null +++ b/tim/prune/save/FileSaver.java @@ -0,0 +1,461 @@ +package tim.prune.save; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.Coordinate; +import tim.prune.data.DataPoint; +import tim.prune.data.Field; +import tim.prune.data.FieldList; +import tim.prune.data.Track; +import tim.prune.load.OneCharDocument; + +/** + * Class to manage the saving of track data + * into a user-specified file + */ +public class FileSaver +{ + private App _app = null; + private JFrame _parentFrame = null; + private Track _track = null; + private JDialog _dialog = null; + private JFileChooser _fileChooser = null; + private JPanel _cards = null; + private JButton _nextButton = null, _backButton = null; + private JTable _table = null; + private FieldSelectionTableModel _model = null; + private JButton _moveUpButton = null, _moveDownButton = null; + private JRadioButton[] _delimiterRadios = null; + private JTextField _otherDelimiterText = null; + private JRadioButton[] _coordUnitsRadios = null; + private JRadioButton[] _altitudeUnitsRadios = null; + private static final int[] FORMAT_COORDS = {Coordinate.FORMAT_NONE, Coordinate.FORMAT_DEG_MIN_SEC, + Coordinate.FORMAT_DEG_MIN, Coordinate.FORMAT_DEG}; + private static final int[] FORMAT_ALTS = {Altitude.FORMAT_NONE, Altitude.FORMAT_METRES, Altitude.FORMAT_FEET}; + + + /** + * Constructor + * @param inApp application object to inform of success + * @param inParentFrame parent frame + * @param inTrack track object to save + */ + public FileSaver(App inApp, JFrame inParentFrame, Track inTrack) + { + _app = inApp; + _parentFrame = inParentFrame; + _track = inTrack; + } + + + /** + * Show the save file dialog + * @param inDefaultDelimiter default delimiter to use + */ + public void showDialog(char inDefaultDelimiter) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + // Check field list + FieldList fieldList = _track.getFieldList(); + int numFields = fieldList.getNumFields(); + _model = new FieldSelectionTableModel(numFields); + for (int i=0; i 0) + { + _model.swapItems(row, row - 1); + _table.setRowSelectionInterval(row - 1, row - 1); + } + } + }); + _moveUpButton.setEnabled(false); + updownPanel.add(_moveUpButton); + _moveDownButton = new JButton(I18nManager.getText("button.movedown")); + _moveDownButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + int row = _table.getSelectedRow(); + if (row > -1 && row < (_model.getRowCount() - 1)) + { + _model.swapItems(row, row + 1); + _table.setRowSelectionInterval(row + 1, row + 1); + } + } + }); + _moveDownButton.setEnabled(false); + updownPanel.add(_moveDownButton); + fieldsPanel.add(updownPanel, BorderLayout.EAST); + // enable/disable buttons based on table row selection + _table.getSelectionModel().addListSelectionListener( + new UpDownToggler(_moveUpButton, _moveDownButton, inTableModel.getRowCount()) + ); + + // Add fields panel and the delimiter panel to first card in pack + JLabel saveOptionsLabel = new JLabel(I18nManager.getText("dialog.save.fieldstosave")); + saveOptionsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + firstCard.add(saveOptionsLabel); + fieldsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT); + firstCard.add(fieldsPanel); + firstCard.add(Box.createRigidArea(new Dimension(0,10))); + + // delimiter panel + JLabel delimLabel = new JLabel(I18nManager.getText("dialog.delimiter.label")); + delimLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + firstCard.add(delimLabel); + JPanel delimsPanel = new JPanel(); + delimsPanel.setLayout(new GridLayout(0, 2)); + delimsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT); + // radio buttons + _delimiterRadios = new JRadioButton[5]; + _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma")); + delimsPanel.add(_delimiterRadios[0]); + _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab")); + delimsPanel.add(_delimiterRadios[1]); + _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon")); + delimsPanel.add(_delimiterRadios[2]); + _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space")); + delimsPanel.add(_delimiterRadios[3]); + JPanel otherPanel = new JPanel(); + otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other")); + otherPanel.add(_delimiterRadios[4]); + _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2); + otherPanel.add(_otherDelimiterText); + // Group radio buttons + ButtonGroup delimGroup = new ButtonGroup(); + for (int i=0; i<_delimiterRadios.length; i++) + { + delimGroup.add(_delimiterRadios[i]); + } + // choose last-used delimiter as default + switch (inDelimiter) + { + case ',' : _delimiterRadios[0].setSelected(true); break; + case '\t' : _delimiterRadios[1].setSelected(true); break; + case ';' : _delimiterRadios[2].setSelected(true); break; + case ' ' : _delimiterRadios[3].setSelected(true); break; + default : _delimiterRadios[4].setSelected(true); + _otherDelimiterText.setText("" + inDelimiter); + } + delimsPanel.add(otherPanel); + firstCard.add(delimsPanel); + _cards.add(firstCard, "card1"); + + JPanel secondCard = new JPanel(); + secondCard.setLayout(new BorderLayout()); + JPanel secondCardHolder = new JPanel(); + secondCardHolder.setLayout(new BoxLayout(secondCardHolder, BoxLayout.Y_AXIS)); + JLabel coordLabel = new JLabel(I18nManager.getText("dialog.save.coordinateunits")); + coordLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + secondCardHolder.add(coordLabel); + JPanel coordsUnitsPanel = new JPanel(); + coordsUnitsPanel.setBorder(BorderFactory.createEtchedBorder()); + coordsUnitsPanel.setLayout(new GridLayout(0, 2)); + _coordUnitsRadios = new JRadioButton[4]; + _coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original")); + _coordUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.degminsec")); + _coordUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.degmin")); + _coordUnitsRadios[3] = new JRadioButton(I18nManager.getText("units.deg")); + ButtonGroup coordGroup = new ButtonGroup(); + for (int i=0; i<4; i++) + { + coordGroup.add(_coordUnitsRadios[i]); + coordsUnitsPanel.add(_coordUnitsRadios[i]); + _coordUnitsRadios[i].setSelected(i==0); + } + coordsUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT); + secondCardHolder.add(coordsUnitsPanel); + secondCardHolder.add(Box.createRigidArea(new Dimension(0,10))); + JLabel altUnitsLabel = new JLabel(I18nManager.getText("dialog.save.altitudeunits")); + altUnitsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + secondCardHolder.add(altUnitsLabel); + JPanel altUnitsPanel = new JPanel(); + altUnitsPanel.setBorder(BorderFactory.createEtchedBorder()); + altUnitsPanel.setLayout(new GridLayout(0, 2)); + _altitudeUnitsRadios = new JRadioButton[3]; + _altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original")); + _altitudeUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.metres")); + _altitudeUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.feet")); + ButtonGroup altGroup = new ButtonGroup(); + for (int i=0; i<3; i++) + { + altGroup.add(_altitudeUnitsRadios[i]); + altUnitsPanel.add(_altitudeUnitsRadios[i]); + _altitudeUnitsRadios[i].setSelected(i==0); + } + altUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT); + secondCardHolder.add(altUnitsPanel); + secondCard.add(secondCardHolder, BorderLayout.NORTH); + _cards.add(secondCard, "card2"); + + // Put together with ok/cancel buttons on the bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + _backButton = new JButton(I18nManager.getText("button.back")); + _backButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + CardLayout cl = (CardLayout)(_cards.getLayout()); + cl.previous(_cards); + _backButton.setEnabled(false); + _nextButton.setEnabled(true); + } + }); + _backButton.setEnabled(false); + buttonPanel.add(_backButton); + _nextButton = new JButton(I18nManager.getText("button.next")); + _nextButton.setEnabled(true); + _nextButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + CardLayout cl = (CardLayout)(_cards.getLayout()); + cl.next(_cards); + _backButton.setEnabled(true); + _nextButton.setEnabled(false); + } + }); + buttonPanel.add(_nextButton); + JButton okButton = new JButton(I18nManager.getText("button.finish")); + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + if (saveToFile()) + { + _dialog.dispose(); + } + } + }); + buttonPanel.add(okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + return panel; + } + + + /** + * Save the track to file with the chosen options + * @return true if successful or cancelled, false if failed + */ + private boolean saveToFile() + { + boolean saveOK = true; + FileWriter writer = null; + if (_fileChooser == null) + _fileChooser = new JFileChooser(); + _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); + if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + File saveFile = _fileChooser.getSelectedFile(); + String lineSeparator = System.getProperty("line.separator"); + // Get coordinate format and altitude format + int coordFormat = Coordinate.FORMAT_NONE; + for (int i=0; i<_coordUnitsRadios.length; i++) + if (_coordUnitsRadios[i].isSelected()) + coordFormat = FORMAT_COORDS[i]; + int altitudeFormat = Altitude.FORMAT_NONE; + for (int i=0; i<_altitudeUnitsRadios.length; i++) + if (_altitudeUnitsRadios[i].isSelected()) + altitudeFormat = FORMAT_ALTS[i]; + + // Check if file exists, don't overwrite any files for v1! + if (!saveFile.exists()) + { + try + { + // Create output file + writer = new FileWriter(saveFile); + // Determine delimiter character to use + char delimiter = getDelimiter(); + FieldInfo info = null; + Field field = null; + StringBuffer buffer = null; + // For now, just spit out to console + int numPoints = _track.getNumPoints(); + int numFields = _model.getRowCount(); + for (int p=0; p\n\n\n"); + writer.write("\t"); + writer.write(inDescription); + writer.write("\n"); + + int i = 0; + DataPoint point = null; + boolean hasTrackpoints = false; + // Loop over waypoints + int numPoints = _track.getNumPoints(); + for (i=0; i\n\t\ttrack\n\t\t\n\t\t\n\t\t\t"); + // Loop over track points + for (i=0; i\n\t\t\n\t"); + } + writer.write("\n"); + writer.close(); + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1") + + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2") + + inFile.getAbsolutePath(), + I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE); + return true; + } + catch (IOException ioe) + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(), + I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + return false; + } + + + /** + * Export the specified waypoint into the file + * @param inPoint waypoint to export + * @param inWriter writer object + */ + private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException + { + inWriter.write("\t\n\t\t"); + inWriter.write(inPoint.getFieldValue(Field.WAYPT_NAME).trim()); + inWriter.write("\n"); + inWriter.write("\t\t\n\t\t\t"); + inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL)); + inWriter.write(','); + inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL)); + inWriter.write(",0\n\t\t\n\t\n"); + } + + + /** + * Export the specified trackpoint into the file + * @param inPoint trackpoint to export + * @param inWriter writer object + */ + private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException + { + inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL)); + inWriter.write(','); + inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL)); + // Altitude not exported, locked to ground by Google Earth + inWriter.write(",0\n"); + } +} diff --git a/tim/prune/save/UpDownToggler.java b/tim/prune/save/UpDownToggler.java new file mode 100644 index 0000000..274bfe9 --- /dev/null +++ b/tim/prune/save/UpDownToggler.java @@ -0,0 +1,52 @@ +package tim.prune.save; + +import javax.swing.JButton; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + + +/** + * Class to enable and disable a pair of up and down buttons + */ +public class UpDownToggler implements ListSelectionListener +{ + private JButton _upButton = null; + private JButton _downButton = null; + private int _maxIndex = 0; + + /** + * Constructor giving buttons and size + * @param inUpButton up button + * @param inDownButton down button + * @param inListSize size of list + */ + public UpDownToggler(JButton inUpButton, JButton inDownButton, int inListSize) + { + _upButton = inUpButton; + _downButton = inDownButton; + _maxIndex = inListSize - 1; + } + + + /** + * list selection has changed + */ + public void valueChanged(ListSelectionEvent e) + { + ListSelectionModel lsm = (ListSelectionModel) e.getSource(); + if (lsm.isSelectionEmpty()) + { + // no rows are selected + _upButton.setEnabled(false); + _downButton.setEnabled(false); + } + else + { + // single row is selected + int row = lsm.getMinSelectionIndex(); + _upButton.setEnabled(row > 0); + _downButton.setEnabled(row >= 0 && row < _maxIndex); + } + } +} diff --git a/tim/prune/undo/UndoCompress.java b/tim/prune/undo/UndoCompress.java new file mode 100644 index 0000000..843054d --- /dev/null +++ b/tim/prune/undo/UndoCompress.java @@ -0,0 +1,61 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a track compression + */ +public class UndoCompress implements UndoOperation +{ + private DataPoint[] _contents = null; + protected int _numPointsDeleted = -1; + + + /** + * Constructor + * @param inTrack track contents to copy + */ + public UndoCompress(Track inTrack) + { + _contents = inTrack.cloneContents(); + } + + + /** + * Set the number of points deleted + * (only known after attempted compression) + * @param inNum number of points deleted + */ + public void setNumPointsDeleted(int inNum) + { + _numPointsDeleted = inNum; + } + + + /** + * @return description of operation including parameters + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.compress"); + if (_numPointsDeleted > 0) + desc = desc + " (" + _numPointsDeleted + ")"; + return desc; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // restore track to previous values + inTrackInfo.getTrack().replaceContents(_contents); + // clear selection + inTrackInfo.getSelection().clearAll(); + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoDeleteDuplicates.java b/tim/prune/undo/UndoDeleteDuplicates.java new file mode 100644 index 0000000..ee11a8f --- /dev/null +++ b/tim/prune/undo/UndoDeleteDuplicates.java @@ -0,0 +1,32 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.Track; + +/** + * Undo duplicate deletion + */ +public class UndoDeleteDuplicates extends UndoCompress +{ + + /** + * Constructor + * @param inTrack Track object + */ + public UndoDeleteDuplicates(Track inTrack) + { + super(inTrack); + } + + + /** + * @return description of operation including parameters + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.deleteduplicates"); + if (_numPointsDeleted > 0) + desc = desc + " (" + _numPointsDeleted + ")"; + return desc; + } +} diff --git a/tim/prune/undo/UndoDeletePoint.java b/tim/prune/undo/UndoDeletePoint.java new file mode 100644 index 0000000..ebc4d89 --- /dev/null +++ b/tim/prune/undo/UndoDeletePoint.java @@ -0,0 +1,54 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Field; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a delete of a single point + */ +public class UndoDeletePoint implements UndoOperation +{ + private int _pointIndex = -1; + private DataPoint _point = null; + + + /** + * Constructor + * @param inIndex index number of point within track + * @param inPoint data point + */ + public UndoDeletePoint(int inIndex, DataPoint inPoint) + { + _pointIndex = inIndex; + _point = inPoint; + } + + + /** + * @return description of operation including point name if any + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.deletepoint"); + String pointName = _point.getFieldValue(Field.WAYPT_NAME); + if (pointName != null && !pointName.equals("")) + desc = desc + " " + pointName; + return desc; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrack Track object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // restore point into track + if (!inTrackInfo.getTrack().insertPoint(_point, _pointIndex)) + { + throw new UndoException(getDescription()); + } + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoDeleteRange.java b/tim/prune/undo/UndoDeleteRange.java new file mode 100644 index 0000000..6bb0b7d --- /dev/null +++ b/tim/prune/undo/UndoDeleteRange.java @@ -0,0 +1,47 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a delete of a range of points + */ +public class UndoDeleteRange implements UndoOperation +{ + private int _startIndex = -1; + private DataPoint[] _points = null; + + + /** + * Constructor + * @param inIndex index number of point within track + * @param inPoint data point + */ + public UndoDeleteRange(TrackInfo inTrackInfo) + { + _startIndex = inTrackInfo.getSelection().getStart(); + _points = inTrackInfo.cloneSelectedRange(); + } + + + /** + * @return description of operation including range length + */ + public String getDescription() + { + return I18nManager.getText("undo.deleterange") + + " (" + _points.length + ")"; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) + { + // restore point array into track + inTrackInfo.getTrack().insertRange(_points, _startIndex); + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoException.java b/tim/prune/undo/UndoException.java new file mode 100644 index 0000000..1c0cebb --- /dev/null +++ b/tim/prune/undo/UndoException.java @@ -0,0 +1,16 @@ +package tim.prune.undo; + +/** + * Exception thrown when undo operation fails + */ +public class UndoException extends Exception +{ + /** + * Constructor + * @param inMessage description of operation which failed + */ + public UndoException(String inMessage) + { + super(inMessage); + } +} diff --git a/tim/prune/undo/UndoInsert.java b/tim/prune/undo/UndoInsert.java new file mode 100644 index 0000000..51da595 --- /dev/null +++ b/tim/prune/undo/UndoInsert.java @@ -0,0 +1,47 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo an insertion (eg interpolate) + */ +public class UndoInsert implements UndoOperation +{ + private int _startPosition = 0; + private int _numInserted = 0; + + + /** + * Constructor + * @param inStart start of insert + * @param inNumInserted number of points inserted + */ + public UndoInsert(int inStart, int inNumInserted) + { + _startPosition = inStart; + _numInserted = inNumInserted; + } + + + /** + * @return description of operation including parameters + */ + public String getDescription() + { + return I18nManager.getText("undo.insert") + " (" + _numInserted + ")"; + } + + + /** + * Perform the undo operation on the given TrackInfo + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // restore track to previous values + inTrackInfo.getTrack().deleteRange(_startPosition, _startPosition + _numInserted - 1); + // reset selection + inTrackInfo.getSelection().select(_startPosition-1, _startPosition-1, _startPosition); + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoLoad.java b/tim/prune/undo/UndoLoad.java new file mode 100644 index 0000000..8bf2218 --- /dev/null +++ b/tim/prune/undo/UndoLoad.java @@ -0,0 +1,88 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a load operation + */ +public class UndoLoad implements UndoOperation +{ + private int _cropIndex = -1; + private int _numLoaded = -1; + private DataPoint[] _contents = null; + private String _previousFilename = null; + + + /** + * Constructor for appending + * @param inIndex index number of crop point + * @param inNumLoaded number of points loaded + */ + public UndoLoad(int inIndex, int inNumLoaded) + { + _cropIndex = inIndex; + _numLoaded = inNumLoaded; + _contents = null; + _previousFilename = null; + } + + + /** + * Constructor for replacing + * @param inOldTrack track being replaced + * @param inNumLoaded number of points loaded + */ + public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded) + { + _cropIndex = -1; + _numLoaded = inNumLoaded; + _contents = inOldTrackInfo.getTrack().cloneContents(); + if (inOldTrackInfo.getFileInfo().getNumFiles() == 1) + _previousFilename = inOldTrackInfo.getFileInfo().getFilename(); + } + + + /** + * @return description of operation including number of points loaded + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.load"); + if (_numLoaded > 0) + desc = desc + " (" + _numLoaded + ")"; + return desc; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // remove file from fileinfo + inTrackInfo.getFileInfo().removeFile(); + if (_previousFilename != null) + { + inTrackInfo.getFileInfo().setFile(_previousFilename); + } + // Crop / replace + if (_contents == null) + { + // crop track to previous size + inTrackInfo.getTrack().cropTo(_cropIndex); + } + else + { + // replace track contents with old + if (!inTrackInfo.getTrack().replaceContents(_contents)) + { + throw new UndoException(getDescription()); + } + } + // clear selection + inTrackInfo.getSelection().clearAll(); + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoOperation.java b/tim/prune/undo/UndoOperation.java new file mode 100644 index 0000000..5efa1fe --- /dev/null +++ b/tim/prune/undo/UndoOperation.java @@ -0,0 +1,21 @@ +package tim.prune.undo; + +import tim.prune.data.TrackInfo; + +/** + * Interface implemented by all Undo Operations + */ +public interface UndoOperation +{ + /** + * Get the description of this operation + * @return description of operation including parameters + */ + public String getDescription(); + + /** + * Perform the undo operation on the specified track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException; +} \ No newline at end of file diff --git a/tim/prune/undo/UndoRearrangeWaypoints.java b/tim/prune/undo/UndoRearrangeWaypoints.java new file mode 100644 index 0000000..d48dc2a --- /dev/null +++ b/tim/prune/undo/UndoRearrangeWaypoints.java @@ -0,0 +1,44 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Track; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a waypoint rearrangement + */ +public class UndoRearrangeWaypoints implements UndoOperation +{ + private DataPoint[] _contents = null; + + + /** + * Constructor + * @param inTrack track contents to copy + */ + public UndoRearrangeWaypoints(Track inTrack) + { + _contents = inTrack.cloneContents(); + } + + + /** + * @return description of operation + */ + public String getDescription() + { + return I18nManager.getText("undo.rearrangewaypoints"); + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // restore track to previous values + inTrackInfo.getTrack().replaceContents(_contents); + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoReverseSection.java b/tim/prune/undo/UndoReverseSection.java new file mode 100644 index 0000000..73fe692 --- /dev/null +++ b/tim/prune/undo/UndoReverseSection.java @@ -0,0 +1,46 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.TrackInfo; + +/** + * Undo reversal of track section + */ +public class UndoReverseSection implements UndoOperation +{ + private int _startIndex, _endIndex; + + + /** + * Constructor + * @param inStart start index of section + * @param inEnd end index of section + */ + public UndoReverseSection(int inStart, int inEnd) + { + _startIndex = inStart; + _endIndex = inEnd; + } + + + /** + * @return description of operation + */ + public String getDescription() + { + return I18nManager.getText("undo.reverse"); + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + if (!inTrackInfo.getTrack().reverseRange(_startIndex, _endIndex)) + { + throw new UndoException(getDescription()); + } + } +} -- 2.43.0