--- /dev/null
+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<inNumUndos; i++)
+ {
+ ((UndoOperation) _undoStack.pop()).performUndo(_trackInfo);
+ }
+ JOptionPane.showMessageDialog(_frame, "" + inNumUndos + " "
+ + (inNumUndos==1?I18nManager.getText("dialog.confirmundo.single.text"):I18nManager.getText("dialog.confirmundo.multiple.text")),
+ I18nManager.getText("dialog.confirmundo.title"),
+ JOptionPane.INFORMATION_MESSAGE);
+ }
+ catch (UndoException ue)
+ {
+ JOptionPane.showMessageDialog(_frame,
+ I18nManager.getText("error.undofailed.text") + " : " + ue.getMessage(),
+ I18nManager.getText("error.undofailed.title"),
+ JOptionPane.ERROR_MESSAGE);
+ _undoStack.clear();
+ _broker.informSubscribers();
+ }
+ catch (EmptyStackException empty) {}
+ }
+
+
+ /**
+ * Helper method to parse an Object into an integer
+ * @param inObject object, eg from dialog
+ * @return int value given
+ */
+ private static int parseNumber(Object inObject)
+ {
+ int num = 0;
+ if (inObject != null)
+ {
+ try
+ {
+ num = Integer.parseInt(inObject.toString());
+ }
+ catch (NumberFormatException nfe)
+ {}
+ }
+ return num;
+ }
+}
--- /dev/null
+package tim.prune;
+
+/**
+ * Interface implemented by clients who want to know
+ * about changes made to the data or its selection
+ */
+public interface DataSubscriber
+{
+ /**
+ * Inform clients that data has been updated
+ */
+ public void dataUpdated();
+
+}
--- /dev/null
+package tim.prune;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.Locale;
+
+import javax.swing.JFrame;
+import javax.swing.JSplitPane;
+import javax.swing.WindowConstants;
+
+import tim.prune.gui.DetailsDisplay;
+import tim.prune.gui.MapChart;
+import tim.prune.gui.MenuManager;
+import tim.prune.gui.ProfileChart;
+
+/**
+ * Tool to visualize, edit and prune GPS data
+ */
+public class GpsPruner
+{
+ public static final String VERSION_NUMBER = "1";
+ public static final String BUILD_NUMBER = "041";
+ private static App APP = null;
+
+
+ /**
+ * Main method
+ * @param args command line arguments
+ */
+ public static void main(String[] args)
+ {
+ Locale locale = null;
+ if (args.length == 1)
+ {
+ if (args[0].startsWith("--locale="))
+ {
+ if (args[0].length() == 11)
+ locale = new Locale(args[0].substring(9));
+ else if (args[0].length() == 14)
+ locale = new Locale(args[0].substring(9, 11), args[0].substring(12));
+ else
+ System.out.println("Unrecognised locale '" + args[0].substring(9)
+ + "' - locale should be eg 'DE' or 'DE_ch'");
+ }
+ else
+ System.out.println("Unknown parameter '" + args[0] +
+ "'. Possible parameters:\n --locale= used for overriding locale settings\n");
+ }
+ I18nManager.init(locale);
+ launch();
+ }
+
+
+ /**
+ * Launch the main application
+ */
+ private static void launch()
+ {
+ JFrame frame = new JFrame("Prune");
+ UpdateMessageBroker broker = new UpdateMessageBroker();
+ APP = new App(frame, broker);
+
+ // make menu
+ MenuManager menuManager = new MenuManager(frame, APP, APP.getTrackInfo());
+ frame.setJMenuBar(menuManager.createMenuBar());
+ APP.setMenuManager(menuManager);
+ broker.addSubscriber(menuManager);
+
+ // Make three GUI components and add as listeners
+ DetailsDisplay leftPanel = new DetailsDisplay(APP, APP.getTrackInfo());
+ broker.addSubscriber(leftPanel);
+ MapChart mapDisp = new MapChart(APP, APP.getTrackInfo());
+ broker.addSubscriber(mapDisp);
+ ProfileChart profileDisp = new ProfileChart(APP.getTrackInfo());
+ broker.addSubscriber(profileDisp);
+
+ JSplitPane rightPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mapDisp, profileDisp);
+ rightPane.setResizeWeight(1.0); // allocate as much space as poss to map
+ frame.getContentPane().add(new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel,
+ rightPane));
+ // add closing listener
+ frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ APP.exit();
+ }
+ });
+ // Avoid automatically shutting down if window closed
+ frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+
+ // finish off and display frame
+ frame.pack();
+ frame.setSize(600, 450);
+ frame.show();
+ }
+}
--- /dev/null
+package tim.prune;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+/**
+ * Manager for all internationalization
+ * Responsible for loading property files
+ * and delivering language-specific texts
+ */
+public abstract class I18nManager
+{
+ private static final String BUNDLE_NAME = "tim.prune.lang.prune-texts";
+ private static final Locale BACKUP_LOCALE = new Locale("en", "GB");
+
+ private static ResourceBundle EnglishTexts = null;
+ private static ResourceBundle ExtraTexts = null;
+
+
+ /**
+ * Initialize the library
+ * using the (optional) locale
+ */
+ public static void init(Locale inLocale)
+ {
+ // Load English texts first to use as defaults
+ EnglishTexts = ResourceBundle.getBundle(BUNDLE_NAME, BACKUP_LOCALE);
+
+ // Get bundle for selected locale, if any
+ if (inLocale != null)
+ {
+ ExtraTexts = ResourceBundle.getBundle(BUNDLE_NAME, inLocale);
+ }
+ else
+ {
+ // locale is null so just use the system default
+ ExtraTexts = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
+ }
+ }
+
+
+ /**
+ * Lookup the given key and return the associated text
+ * @param inKey key to lookup
+ * @return associated text, or the key if not found
+ */
+ public static String getText(String inKey)
+ {
+ String value = null;
+ // look in extra texts if available
+ if (ExtraTexts != null)
+ {
+ try
+ {
+ value = ExtraTexts.getString(inKey);
+ if (value != null && !value.equals(""))
+ return value;
+ }
+ catch (MissingResourceException mre) {}
+ }
+ // look in english texts
+ if (EnglishTexts != null)
+ {
+ try
+ {
+ value = EnglishTexts.getString(inKey);
+ if (value != null && !value.equals(""))
+ return value;
+ }
+ catch (MissingResourceException mre) {}
+ }
+ // return the key itself
+ return inKey;
+ }
+}
--- /dev/null
+package tim.prune;
+
+/**
+ * Class responsible for distributing update information
+ * to all registered listeners
+ */
+public class UpdateMessageBroker
+{
+ private DataSubscriber[] _subscribers;
+ private int _subscriberNum = 0;
+ private static final int MAXIMUM_NUMBER_SUBSCRIBERS = 4;
+
+
+ /**
+ * Constructor
+ * @param inTrack Track object
+ */
+ public UpdateMessageBroker()
+ {
+ _subscribers = new DataSubscriber[MAXIMUM_NUMBER_SUBSCRIBERS];
+ }
+
+
+ /**
+ * Add a data subscriber to the list
+ * @param inSub DataSubscriber to add
+ */
+ public void addSubscriber(DataSubscriber inSub)
+ {
+ _subscribers[_subscriberNum] = inSub;
+ _subscriberNum++;
+ }
+
+
+ /**
+ * Send a message to all subscribers that
+ * the data has been updated
+ */
+ public void informSubscribers()
+ {
+ for (int i=0; i<_subscribers.length; i++)
+ {
+ if (_subscribers[i] != null)
+ {
+ _subscribers[i].dataUpdated();
+ }
+ }
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Class to hold an altitude and provide conversion functions
+ */
+public class Altitude
+{
+ private boolean _valid = false;
+ private int _value = 0;
+ private int _format = -1;
+ public static final int FORMAT_NONE = -1;
+ public static final int FORMAT_METRES = 0;
+ public static final int FORMAT_FEET = 1;
+
+ private static final double CONVERT_FEET_TO_METRES = 0.3048;
+ private static final double CONVERT_METRES_TO_FEET = 3.28084;
+
+
+ /**
+ * Constructor
+ */
+ public Altitude(String inString, int inFormat)
+ {
+ if (inString != null && !inString.equals(""))
+ {
+ try
+ {
+ _value = Integer.parseInt(inString.trim());
+ _format = inFormat;
+ _valid = true;
+ }
+ catch (NumberFormatException nfe) {}
+ }
+ }
+
+
+ /**
+ * Constructor
+ */
+ public Altitude(int inValue, int inFormat)
+ {
+ _value = inValue;
+ _format = inFormat;
+ _valid = true;
+ }
+
+
+ /**
+ * @return true if the value could be parsed
+ */
+ public boolean isValid()
+ {
+ return _valid;
+ }
+
+
+ /**
+ * @return raw value as int
+ */
+ public int getValue()
+ {
+ return _value;
+ }
+
+
+ /**
+ * @return format of number
+ */
+ public int getFormat()
+ {
+ return _format;
+ }
+
+
+ /**
+ * Get the altitude value in the specified format
+ * @param inFormat desired format, either FORMAT_METRES or FORMAT_FEET
+ * @return value as an int
+ */
+ public int getValue(int inFormat)
+ {
+ if (inFormat == _format)
+ return _value;
+ if (inFormat == FORMAT_METRES)
+ return (int) (_value * CONVERT_FEET_TO_METRES);
+ if (inFormat == FORMAT_FEET)
+ return (int) (_value * CONVERT_METRES_TO_FEET);
+ return _value;
+ }
+
+
+ /**
+ * Interpolate a new Altitude object between the given ones
+ * @param inStart start altitude
+ * @param inEnd end altitude
+ * @param inIndex index of interpolated point
+ * @param inNumSteps number of steps to interpolate
+ * @return Interpolated Altitude object
+ */
+ public static Altitude interpolate(Altitude inStart, Altitude inEnd, int inIndex, int inNumSteps)
+ {
+ // Check if altitudes are valid
+ if (inStart == null || inEnd == null || !inStart.isValid() || !inEnd.isValid())
+ return new Altitude(null, FORMAT_NONE);
+ // Use altitude format of first point
+ int altFormat = inStart.getFormat();
+ int startValue = inStart.getValue();
+ int endValue = inEnd.getValue(altFormat);
+ int newValue = startValue
+ + (int) ((endValue - startValue) * 1.0 / (inNumSteps + 1) * (inIndex + 1));
+ return new Altitude(newValue, altFormat);
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Represents a range of altitudes, taking units into account.
+ * Values assumed to be >= 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;
+ }
+}
--- /dev/null
+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<strLen; i++)
+ {
+ currChar = inString.charAt(i);
+ if (currChar >= '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;
+ }
+}
--- /dev/null
+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<inNumPoints; i++)
+ {
+ Coordinate latitude = Coordinate.interpolate(_latitude, endLatitude, i, inNumPoints);
+ Coordinate longitude = Coordinate.interpolate(_longitude, endLongitude, i, inNumPoints);
+ Altitude altitude = Altitude.interpolate(_altitude, endAltitude, i, inNumPoints);
+ range[i] = new DataPoint(latitude, longitude, altitude);
+ }
+ return range;
+ }
+
+
+ /**
+ * Calculate the number of radians between two points (for distance calculation)
+ * @param inPoint1 first point
+ * @param inPoint2 second point
+ * @return angular distance between points in radians
+ */
+ public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
+ {
+ if (inPoint1 == null || inPoint2 == null)
+ return 0.0;
+ final double TO_RADIANS = Math.PI / 180.0;
+ // Get lat and long from points
+ double lat1 = inPoint1.getLatitude().getDouble() * TO_RADIANS;
+ double lat2 = inPoint2.getLatitude().getDouble() * TO_RADIANS;
+ double lon1 = inPoint1.getLongitude().getDouble() * TO_RADIANS;
+ double lon2 = inPoint2.getLongitude().getDouble() * TO_RADIANS;
+ // Formula given by Wikipedia:Great-circle_distance as follows:
+ // angle = 2 arcsin( sqrt( (sin ((lat2-lat1)/2))^^2 + cos(lat1)cos(lat2)(sin((lon2-lon1)/2))^^2))
+ double firstSine = Math.sin((lat2-lat1) / 2.0);
+ double secondSine = Math.sin((lon2-lon1) / 2.0);
+ double term2 = Math.cos(lat1) * Math.cos(lat2) * secondSine * secondSine;
+ double answer = 2 * Math.asin(Math.sqrt(firstSine*firstSine + term2));
+ // phew
+ return answer;
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Class to provide distance constants and functions
+ */
+public abstract class Distance
+{
+ // distance formats
+ public static final int UNITS_KILOMETRES = 1;
+ public static final int UNITS_MILES = 2;
+
+ // Geographical constants
+ private static final double EARTH_RADIUS_KM = 6372.795;
+ private static final double EARTH_RADIUS_MILES = 3959.8712255;
+ // Conversion constants
+ private static final double CONVERT_KM_TO_MILES = 1.609344;
+ private static final double CONVERT_MILES_TO_KM = 0.621371192;
+
+
+ /**
+ * Convert the given angle in radians into a distance
+ * @param inAngDist angular distance in radians
+ * @param inUnits desired units, miles or km
+ * @return distance in specified format
+ */
+ public static double convertRadians(double inAngDist, int inUnits)
+ {
+ // Multiply by appropriate factor
+ if (inUnits == UNITS_MILES)
+ return inAngDist * EARTH_RADIUS_MILES;
+ return inAngDist * EARTH_RADIUS_KM;
+ }
+
+
+
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Represents a range of doubles, holding the maximum and
+ * minimum values. Values can be positive or negative
+ */
+public class DoubleRange
+{
+ private boolean _empty = true;
+ private double _min = 0.0, _max = 0.0;
+
+
+ /**
+ * Add a value to the range
+ * @param inValue value to add
+ */
+ public void addValue(double inValue)
+ {
+ if (inValue < _min || _empty) _min = inValue;
+ if (inValue > _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;
+ }
+}
--- /dev/null
+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()));
+ }
+}
--- /dev/null
+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<inOtherList._fieldArray.length; f++)
+ {
+ if (inOtherList._fieldArray[f] != null && !contains(inOtherList._fieldArray[f]))
+ {
+ totalFields++;
+ }
+ }
+ FieldList list = new FieldList(totalFields);
+ // copy these fields into array
+ System.arraycopy(_fieldArray, 0, list._fieldArray, 0, _fieldArray.length);
+ // copy additional fields from other array if any
+ if (totalFields > _fieldArray.length)
+ {
+ int fieldCounter = _fieldArray.length;
+ for (int f=0; f<inOtherList._fieldArray.length; f++)
+ {
+ if (inOtherList._fieldArray[f] != null && !contains(inOtherList._fieldArray[f]))
+ {
+ list._fieldArray[fieldCounter] = inOtherList._fieldArray[f];
+ fieldCounter++;
+ }
+ }
+ }
+ // return the merged list
+ return list;
+ }
+
+
+ /**
+ * Convert to String for debug
+ */
+ public String toString()
+ {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append("(");
+ for (int i=0; i<_fieldArray.length; i++)
+ {
+ buffer.append(_fieldArray[i].getName()).append(',');
+ }
+ buffer.append(")");
+ return buffer.toString();
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Class to represent a type of field,
+ * for example coordinate or integer
+ */
+public class FieldType
+{
+ private int _id = 0;
+
+ public static final FieldType NONE = new FieldType(0);
+ public static final FieldType INT = new FieldType(1);
+ public static final FieldType BOOL = new FieldType(2);
+ public static final FieldType COORD = new FieldType(3);
+ public static final FieldType TIME = new FieldType(4);
+
+
+ /**
+ * Private constructor
+ * @param inId identifier
+ */
+ private FieldType(int inId)
+ {
+ _id = inId;
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Class to hold the information about the file(s)
+ * from which the data was loaded from / saved to
+ */
+public class FileInfo
+{
+ private String _filename = null;
+ private int _numFiles = 0;
+
+
+ /**
+ * Set the file information to the specified file
+ * @param inFilename filename of file
+ */
+ public void setFile(String inFilename)
+ {
+ _filename = inFilename;
+ _numFiles = 1;
+ }
+
+
+ /**
+ * Add a file to the data
+ */
+ public void addFile()
+ {
+ _numFiles++;
+ }
+
+
+ /**
+ * Undo a load file
+ */
+ public void removeFile()
+ {
+ _numFiles--;
+ }
+
+
+ /**
+ * @return the number of files loaded
+ */
+ public int getNumFiles()
+ {
+ return _numFiles;
+ }
+
+
+ /**
+ * @return The filename, if a single file
+ */
+ public String getFilename()
+ {
+ if (_numFiles == 1 && _filename != null)
+ return _filename;
+ return "";
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+/**
+ * Represents a range of integers, holding the maximum and
+ * minimum values. Values assumed to be >= 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;
+ }
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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<ALL_DATE_FORMATS.length && !_valid; i++)
+ {
+ try
+ {
+ date = ALL_DATE_FORMATS[i].parse(inString);
+ CALENDAR.setTime(date);
+ _seconds = CALENDAR.getTimeInMillis() / 1000L;
+ _valid = true;
+ }
+ catch (ParseException e) {}
+ }
+ }
+ }
+ }
+
+
+ /**
+ * @return true if timestamp is valid
+ */
+ public boolean isValid()
+ {
+ return _valid;
+ }
+
+
+ /**
+ * Calculate the difference between two Timestamps in seconds
+ * @param inOther other, earlier Timestamp
+ * @return number of seconds since other timestamp
+ */
+ public long getSecondsSince(Timestamp inOther)
+ {
+ return _seconds - inOther._seconds;
+ }
+
+
+ /**
+ * @return Description of timestamp in locale-specific format
+ */
+ public String getText()
+ {
+ if (_text == null)
+ {
+ if (_valid)
+ {
+ CALENDAR.setTimeInMillis(_seconds * 1000L);
+ _text = DEFAULT_DATE_FORMAT.format(CALENDAR.getTime());
+ }
+ else _text = "";
+ }
+ return _text;
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+import tim.prune.UpdateMessageBroker;
+
+
+/**
+ * Class to hold all track information,
+ * including track points and waypoints
+ */
+public class Track
+{
+ // Broker object
+ UpdateMessageBroker _broker = null;
+ // Data points
+ private DataPoint[] _dataPoints = null;
+ // Scaled x, y values
+ private double[] _xValues = null;
+ private double[] _yValues = null;
+ private boolean _scaled = false;
+ private int _numPoints = 0;
+ private boolean _mixedData = false;
+ // Master field list
+ private FieldList _masterFieldList = null;
+ // variable ranges
+ private AltitudeRange _altitudeRange = null;
+ private DoubleRange _latRange = null, _longRange = null;
+ private DoubleRange _xRange = null, _yRange = null;
+
+
+ /**
+ * Constructor giving arrays of Fields and Objects
+ * @param inFieldArray field array
+ * @param inPointArray 2d array of field values
+ */
+ public Track(UpdateMessageBroker inBroker)
+ {
+ _broker = inBroker;
+ // create field list
+ _masterFieldList = new FieldList(null);
+ // make empty DataPoint array
+ _dataPoints = new DataPoint[0];
+ _numPoints = 0;
+ // needs to be scaled
+ _scaled = false;
+ }
+
+
+ /**
+ * Load method, for initialising and reinitialising data
+ * @param inFieldArray array of Field objects describing fields
+ * @param inPointArray 2d object array containing data
+ * @param inAltFormat altitude format
+ */
+ public void load(Field[] inFieldArray, Object[][] inPointArray, int inAltFormat)
+ {
+ // copy field list
+ _masterFieldList = new FieldList(inFieldArray);
+ // make DataPoint object from each point in inPointList
+ _dataPoints = new DataPoint[inPointArray.length];
+ String[] dataArray = null;
+ int pointIndex = 0;
+ for (int p=0; p < inPointArray.length; p++)
+ {
+ dataArray = (String[]) inPointArray[p];
+ // Convert to DataPoint objects
+ DataPoint point = new DataPoint(dataArray, _masterFieldList, inAltFormat);
+ if (point.isValid())
+ {
+ _dataPoints[pointIndex] = point;
+ pointIndex++;
+ }
+ }
+ _numPoints = pointIndex;
+ // needs to be scaled
+ _scaled = false;
+ }
+
+
+ ////////////////// Modification methods //////////////////////
+
+
+ /**
+ * Combine this Track with new data
+ * @param inOtherTrack
+ */
+ public void combine(Track inOtherTrack)
+ {
+ // merge field list
+ _masterFieldList = _masterFieldList.merge(inOtherTrack._masterFieldList);
+ // expand data array and add other track's data points
+ int totalPoints = getNumPoints() + inOtherTrack.getNumPoints();
+ DataPoint[] mergedPoints = new DataPoint[totalPoints];
+ System.arraycopy(_dataPoints, 0, mergedPoints, 0, getNumPoints());
+ System.arraycopy(inOtherTrack._dataPoints, 0, mergedPoints, getNumPoints(), inOtherTrack.getNumPoints());
+ _dataPoints = mergedPoints;
+ // combine point count
+ _numPoints = totalPoints;
+ // needs to be scaled again
+ _scaled = false;
+ // inform listeners
+ _broker.informSubscribers();
+ }
+
+
+ /**
+ * Crop the track to the given size - subsequent points are not (yet) deleted
+ * @param inNewSize new number of points in track
+ */
+ public void cropTo(int inNewSize)
+ {
+ if (inNewSize >= 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<numCopied && keepPoint; j++)
+ {
+ // calculate distance between point j and current point
+ double pointDist = Math.abs(_xValues[i] - _xValues[pointIndices[j]])
+ + Math.abs(_yValues[i] - _yValues[pointIndices[j]]);
+ if (pointDist < minDist)
+ keepPoint = false;
+ }
+ }
+ if (keepPoint)
+ {
+ newPointArray[numCopied] = _dataPoints[i];
+ pointIndices[numCopied] = i;
+ numCopied++;
+ }
+ }
+
+ // Copy array references
+ int numDeleted = _numPoints - numCopied;
+ if (numDeleted > 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<newSize; i++)
+ newPointArray[i] = _dataPoints[i*2];
+ // Copy array references
+ _dataPoints = newPointArray;
+ _numPoints = _dataPoints.length;
+ _scaled = false;
+ _broker.informSubscribers();
+ return numDeleted;
+ }
+
+
+ /**
+ * Delete the specified point
+ * @return true if successful
+ */
+ public boolean deletePoint(int inIndex)
+ {
+ boolean answer = deleteRange(inIndex, inIndex);
+ return answer;
+ }
+
+
+ /**
+ * Delete the specified range of points from the Track
+ * @param inStart start of range (inclusive)
+ * @param inEnd end of range (inclusive)
+ * @return true if successful
+ */
+ public boolean deleteRange(int inStart, int inEnd)
+ {
+ if (inStart < 0 || inEnd < 0 || inEnd < inStart)
+ {
+ // no valid range selected so can't delete
+ return false;
+ }
+ // valid range, let's delete it
+ int numToDelete = inEnd - inStart + 1;
+ DataPoint[] newPointArray = new DataPoint[_numPoints - numToDelete];
+ // Copy points before the selected range
+ if (inStart > 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<i && !dupes[i]; j++)
+ {
+ DataPoint p2 = _dataPoints[j];
+ if (p1.isDuplicate(p2))
+ {
+ dupes[i] = true;
+ numDupes++;
+ }
+ }
+ }
+ if (numDupes > 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<numPointsToReverse; i++)
+ {
+ // swap pairs of points
+ p = _dataPoints[inStart + i];
+ _dataPoints[inStart + i] = _dataPoints[inEnd - i];
+ _dataPoints[inEnd - i] = p;
+ }
+ // needs to be scaled again
+ _scaled = false;
+ _broker.informSubscribers();
+ return true;
+ }
+
+
+ /**
+ * Collect all waypoints to the start or end of the track
+ * @param inAtStart true to collect at start, false for end
+ * @return true if successful, false if no change
+ */
+ public boolean collectWaypoints(boolean inAtStart)
+ {
+ // Check for mixed data, numbers of waypoints & nons
+ int numWaypoints = 0, numNonWaypoints = 0;
+ boolean wayAfterNon = false, nonAfterWay = false;
+ DataPoint[] waypoints = new DataPoint[_numPoints];
+ DataPoint[] nonWaypoints = new DataPoint[_numPoints];
+ DataPoint point = null;
+ for (int i=0; i<_numPoints; i++)
+ {
+ point = _dataPoints[i];
+ if (point.isWaypoint())
+ {
+ waypoints[numWaypoints] = point;
+ numWaypoints++;
+ wayAfterNon |= (numNonWaypoints > 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<numWaypoints; j++)
+ {
+ if (pointIndices[j] == i)
+ {
+ dataCopy[copyIndex] = waypoints[j];
+ copyIndex++;
+ }
+ }
+ }
+ // Copy data back to track
+ _dataPoints = dataCopy;
+ // needs to be scaled again to recalc x, y
+ _scaled = false;
+ _broker.informSubscribers();
+ return true;
+ }
+
+
+ /**
+ * 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 inStartIndex, int inNumPoints)
+ {
+ // check parameters
+ if (inStartIndex < 0 || inStartIndex >= _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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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("<p>").append(I18nManager.getText("dialog.about.summarytext1")).append("</p>");
+ descBuffer.append("<p>").append(I18nManager.getText("dialog.about.summarytext2")).append("</p>");
+ descBuffer.append("<p>").append(I18nManager.getText("dialog.about.summarytext3")).append("</p>");
+ 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();
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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)
+ {}
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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<numPoints; i++)
+ {
+ x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
+ y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
+ if (x > 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<numPoints; i++)
+ {
+ DataPoint point = _track.getPoint(i);
+ String waypointName = point.getFieldValue(Field.WAYPT_NAME);
+ if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES)
+ {
+ x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
+ y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
+ if (x > 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<inWidth; x++)
+ {
+ for (int y=0; y<inHeight; y++)
+ {
+ int pixelColor = _image.getRGB(inX + x, inY - y);
+ if (pixelColor != -1) return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Make the popup menu for right-clicking the map
+ */
+ private void makePopup()
+ {
+ _popup = new JPopupMenu();
+ JMenuItem zoomIn = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
+ zoomIn.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ zoomMap(true);
+ }});
+ zoomIn.setEnabled(true);
+ _popup.add(zoomIn);
+ JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
+ zoomOut.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ zoomMap(false);
+ }});
+ zoomOut.setEnabled(true);
+ _popup.add(zoomOut);
+ JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
+ zoomFull.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ zoomToFullScale();
+ }});
+ zoomFull.setEnabled(true);
+ _popup.add(zoomFull);
+ _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
+ _autoPanMenuItem.setSelected(true);
+ _popup.add(_autoPanMenuItem);
+ }
+
+
+ /**
+ * Zoom map to full scale
+ */
+ private void zoomToFullScale()
+ {
+ _zoomScale = 1.0;
+ _offsetX = 0.0;
+ _offsetY = 0.0;
+ _numPoints = 0;
+ dataUpdated();
+ }
+
+
+ /**
+ * Zoom map either in or out by one step
+ * @param inZoomIn true to zoom in, false for out
+ */
+ private void zoomMap(boolean inZoomIn)
+ {
+ if (inZoomIn)
+ {
+ // Zoom in
+ _zoomScale *= ZOOM_SCALE_FACTOR;
+ }
+ else
+ {
+ // Zoom out
+ _zoomScale /= ZOOM_SCALE_FACTOR;
+ if (_zoomScale < 0.5) _zoomScale = 0.5;
+ }
+ _numPoints = 0;
+ dataUpdated();
+ }
+
+
+ /**
+ * Pan the map by the specified amounts
+ * @param inUp upwards pan
+ * @param inRight rightwards pan
+ */
+ private void panMap(int inUp, int inRight)
+ {
+ double panFactor = _scale / _zoomScale;
+ _offsetY = _offsetY + (inUp * panFactor);
+ _offsetX = _offsetX - (inRight * panFactor);
+ // Limit pan to sensible range??
+ _numPoints = 0;
+ dataUpdated();
+ }
+
+
+ /**
+ * React to click on map display
+ */
+ public void mouseClicked(MouseEvent e)
+ {
+ this.requestFocus();
+ if (_track != null)
+ {
+ 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))
+ {
+ // 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
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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<numScales; i++)
+ {
+ scale = ALTITUDE_SCALES[i];
+ if (scale < inMax)
+ {
+ numLines = 0;
+ altitude = 0;
+ while (altitude < inMax)
+ {
+ altitude += scale;
+ if (altitude > 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);
+ }
+ }
+ }
+}
--- /dev/null
+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<undoStack.size(); i++)
+ {
+ undoActions[i] = ((UndoOperation) undoStack.elementAt(undoStack.size()-1-i)).getDescription();
+ }
+ _actionList = new JList(undoActions);
+ _actionList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+ _actionList.setSelectedIndex(0);
+ _actionList.addListSelectionListener(new ListSelectionListener()
+ {
+ public void valueChanged(ListSelectionEvent e)
+ {
+ if (_actionList.getMinSelectionIndex() > 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();
+ }
+
+}
--- /dev/null
+# 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.<br>Copying, redistribution and modification are permitted and encouraged<br>according to the conditions in the included <code>license.txt</code> file.
+dialog.about.summarytext3=Please see <code style="font-weight:bold">http://activityworkshop.net/</code> 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.
--- /dev/null
+# 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.<br>Kopieren, Weiterverbreitung und Veränderungen sind erlaubt und willkommen<br>unter die Bedingungen im enthaltenen <code>license.txt</code> File.
+dialog.about.summarytext3=Bitte sehen Sie <code style="font-weight:bold">http://activityworkshop.net/</code> 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.
--- /dev/null
+# 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.<br>Kopiere, Wiiterverbreitig und Veränderige sin erlaubt und willkommen<br>unter die Bedingunge im enthaltene <code>license.txt</code> File.
+dialog.about.summarytext3=Bitte lueg na <code style="font-weight:bold">http://activityworkshop.net/</code> 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.
--- /dev/null
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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.
--- /dev/null
+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 + ")";
+ }
+}
--- /dev/null
+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<Field.ALL_AVAILABLE_FIELDS.length; i++)
+ if (Field.ALL_AVAILABLE_FIELDS[i].getName().equals(inValue))
+ _fieldArray[inRow] = Field.ALL_AVAILABLE_FIELDS[i];
+ }
+ }
+ // fire change
+ fireTableRowsUpdated(inRow, inRow);
+ }
+
+
+ /**
+ * @return array of Field objects
+ */
+ public Field[] getFieldArray()
+ {
+ return _fieldArray;
+ }
+
+
+ /**
+ * @param inName Name of field to find
+ * @return true if this field is already present
+ */
+ private boolean hasField(String inName)
+ {
+ if (_fieldArray == null || inName == null) return false;
+ for (int i=0; i<_numRows; i++)
+ if (_fieldArray[i].getName().equals(inName))
+ return true;
+ return false;
+ }
+}
--- /dev/null
+package tim.prune.load;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Class to load the contents of a file
+ * into an array for later retrieval
+ */
+public class FileCacher
+{
+ private File _file = null;
+ private String[] _contentArray = null;
+
+
+ /**
+ * Constructor
+ * @param inFile File object to cache
+ */
+ public FileCacher(File inFile)
+ {
+ _file = inFile;
+ loadFile();
+ }
+
+
+ /**
+ * Load the specified file into memory
+ */
+ private void loadFile()
+ {
+ ArrayList contentList = new ArrayList();
+ if (_file != null && _file.exists() && _file.canRead())
+ {
+ BufferedReader reader = null;
+ try
+ {
+ reader = new BufferedReader(new FileReader(_file));
+ String currLine = reader.readLine();
+ while (currLine != null)
+ {
+ if (currLine.trim().length() > 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<numLines; i++)
+ _contentArray[i] = contentList.get(i).toString();
+ }
+
+
+ /**
+ * @return Contents of the file as array of non-blank Strings
+ */
+ public String[] getContents()
+ {
+ return _contentArray;
+ }
+
+
+ /**
+ * Get the top section of the file for preview
+ * @param inSize number of lines to extract
+ * @return String array containing non-blank lines from the file
+ */
+ public String[] getSnippet(int inNumRows, int inMaxWidth)
+ {
+ final int MIN_SNIPPET_SIZE = 3;
+ // Check size is within sensible limits
+ int numToCopy = inNumRows;
+ if (numToCopy > 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<size; i++)
+ {
+ if (result[i].length() > 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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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<fileContents.length && fileOK; lineNum++)
+ {
+ currLine = fileContents[lineNum];
+ // check for invalid characters
+ if (currLine.indexOf('\0') >= 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<Field.ALL_AVAILABLE_FIELDS.length; i++)
+ {
+ fieldTypesBox.addItem(Field.ALL_AVAILABLE_FIELDS[i].getName());
+ }
+ _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
+
+ // Set altitude format to same as last time if available
+ if (_lastAltitudeFormat == Altitude.FORMAT_METRES)
+ _unitsDropDown.setSelectedIndex(0);
+ else if (_lastAltitudeFormat == Altitude.FORMAT_FEET)
+ _unitsDropDown.setSelectedIndex(1);
+ // no selection on field list
+ selectField(-1);
+ }
+
+
+ /**
+ * All options have been selected, so load file
+ */
+ private void finished()
+ {
+ // Save delimiter, field array and altitude format for later use
+ _lastUsedDelimiter = _currentDelimiter;
+ _lastSelectedFields = _fieldTableModel.getFieldArray();
+ int altitudeFormat = Altitude.FORMAT_METRES;
+ if (_unitsDropDown.getSelectedIndex() == 1)
+ {
+ altitudeFormat = Altitude.FORMAT_FEET;
+ }
+ _lastAltitudeFormat = altitudeFormat;
+ // give data to App
+ _app.informDataLoaded(_fieldTableModel.getFieldArray(),
+ _fileExtractTableModel.getData(), altitudeFormat,
+ _file.getName());
+ // clear up file cacher
+ _fileCacher.clear();
+ // dispose of dialog
+ _dialog.dispose();
+ }
+
+
+ /**
+ * Make a panel with a label and a component
+ * @param inLabelKey label key to use
+ * @param inComponent component for main area of panel
+ * @return labelled Panel
+ */
+ private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
+ {
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
+ panel.add(inComponent, BorderLayout.CENTER);
+ return panel;
+ }
+
+
+ /**
+ * An entry in the field list has been selected
+ * @param inFieldNum index of field, starting with 0
+ */
+ private void selectField(int inFieldNum)
+ {
+ if (inFieldNum == -1 || inFieldNum != _selectedField)
+ {
+ _selectedField = inFieldNum;
+ _moveUpButton.setEnabled(inFieldNum > 0);
+ _moveDownButton.setEnabled(inFieldNum >= 0
+ && inFieldNum < (_fieldTableModel.getRowCount()-1));
+ }
+ }
+
+
+ /**
+ * @return the last delimiter character used for a load
+ */
+ public char getLastUsedDelimiter()
+ {
+ return _lastUsedDelimiter;
+ }
+}
--- /dev/null
+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<contents.length; i++)
+ {
+ if (contents[i] != null && !contents[i].trim().equals(""))
+ {
+ _numRows++;
+ String[] splitLine = contents[i].split(delimStr);
+ if (splitLine != null && splitLine.length > 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<contents.length; i++)
+ {
+ result[i] = new String[maxFields];
+ if (contents[i] != null)
+ {
+ String wholeLine = contents[i].trim();
+ if (!wholeLine.equals(""))
+ {
+ String[] splitLine = wholeLine.split(delimStr);
+ if (splitLine != null)
+ {
+ System.arraycopy(splitLine, 0, result[i], 0, splitLine.length);
+ // Check if columns are blank or not
+ for (int j=0; j<splitLine.length; j++)
+ {
+ if (!_columnStates[j] && splitLine[j].trim().length() > 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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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.
--- /dev/null
+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)":"(---)");
+ }
+}
--- /dev/null
+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];
+ }
+}
--- /dev/null
+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<numFields; i++)
+ {
+ Field field = fieldList.getField(i);
+ FieldInfo info = new FieldInfo(field, _track.hasData(field));
+ _model.addFieldInfo(info, i);
+ }
+ _dialog.getContentPane().add(makeDialogComponents(_model, inDefaultDelimiter));
+ _dialog.pack();
+ _dialog.show();
+ }
+
+
+ /**
+ * Make the dialog components
+ * @param inTableModel table model for fields
+ * @param inDelimiter default delimiter character
+ * @return the GUI components for the save dialog
+ */
+ private Component makeDialogComponents(FieldSelectionTableModel inTableModel, char inDelimiter)
+ {
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ _cards = new JPanel();
+ _cards.setLayout(new CardLayout());
+ panel.add(_cards, BorderLayout.CENTER);
+
+ // Make first card for field selection and delimiter
+ JPanel firstCard = new JPanel();
+ firstCard.setLayout(new BoxLayout(firstCard, BoxLayout.Y_AXIS));
+ JPanel tablePanel = new JPanel();
+ tablePanel.setLayout(new BorderLayout());
+ _table = new JTable(inTableModel);
+ _table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ tablePanel.add(_table.getTableHeader(), BorderLayout.NORTH);
+ tablePanel.add(_table, BorderLayout.CENTER);
+
+ // Make a panel to hold the table and up/down buttons
+ JPanel fieldsPanel = new JPanel();
+ fieldsPanel.setLayout(new BorderLayout());
+ fieldsPanel.add(tablePanel, BorderLayout.CENTER);
+ JPanel updownPanel = new JPanel();
+ updownPanel.setLayout(new BoxLayout(updownPanel, BoxLayout.Y_AXIS));
+ _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
+ _moveUpButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ int row = _table.getSelectedRow();
+ if (row > 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<numPoints; p++)
+ {
+ DataPoint point = _track.getPoint(p);
+ boolean firstField = true;
+ buffer = new StringBuffer();
+ for (int f=0; f<numFields; f++)
+ {
+ info = _model.getFieldInfo(f);
+ if (info.isSelected())
+ {
+ if (!firstField)
+ {
+ // output field separator
+ buffer.append(delimiter);
+ }
+ field = info.getField();
+ // Output field according to type
+ if (field == Field.LATITUDE)
+ {
+ buffer.append(point.getLatitude().output(coordFormat));
+ }
+ else if (field == Field.LONGITUDE)
+ {
+ buffer.append(point.getLongitude().output(coordFormat));
+ }
+ else if (field == Field.ALTITUDE)
+ {
+ buffer.append(point.getAltitude().getValue(altitudeFormat));
+ }
+ else if (field == Field.TIMESTAMP)
+ {
+ buffer.append(point.getTimestamp().getText());
+ }
+ else
+ {
+ String value = point.getFieldValue(field);
+ if (value != null)
+ {
+ buffer.append(value);
+ }
+ }
+ firstField = false;
+ }
+ }
+ // Output to file
+ writer.write(buffer.toString());
+ writer.write(lineSeparator);
+ }
+ // Save successful
+ JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
+ + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2")
+ + saveFile.getAbsolutePath(),
+ I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
+ _app.informDataSaved();
+ }
+ catch (IOException ioe)
+ {
+ saveOK = false;
+ JOptionPane.showMessageDialog(_parentFrame,
+ I18nManager.getText("error.save.failed") + ioe.getMessage(),
+ I18nManager.getText("error.save.dialogtitle"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+ finally
+ {
+ // try to close file if it's open
+ try
+ {
+ if (writer != null)
+ {
+ writer.close();
+ }
+ }
+ catch (Exception e) {}
+ }
+ }
+ else
+ {
+ saveOK = false;
+ JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.fileexists"),
+ I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ return saveOK;
+ }
+
+
+ /**
+ * @return the selected delimiter character
+ */
+ private char getDelimiter()
+ {
+ // Check the preset 4 delimiters
+ final char[] delimiters = {',', '\t', ';', ' '};
+ for (int i=0; i<4; i++)
+ {
+ if (_delimiterRadios[i].isSelected())
+ {
+ return delimiters[i];
+ }
+ }
+ // Wasn't any of those so must be 'other'
+ return _otherDelimiterText.getText().charAt(0);
+ }
+}
--- /dev/null
+package tim.prune.save;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.swing.filechooser.FileFilter;
+
+import tim.prune.App;
+import tim.prune.I18nManager;
+import tim.prune.data.Coordinate;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Field;
+import tim.prune.data.Track;
+
+/**
+ * Class to export track information
+ * into a specified Kml file
+ */
+public class KmlExporter
+{
+ private App _app = null;
+ private JFrame _parentFrame = null;
+ private Track _track = null;
+ private JFileChooser _fileChooser = null;
+
+
+ /**
+ * Constructor giving App object, frame and track
+ * @param inApp application object to inform of success
+ * @param inParentFrame parent frame
+ * @param inTrack track object to save
+ */
+ public KmlExporter(App inApp, JFrame inParentFrame, Track inTrack)
+ {
+ _app = inApp;
+ _parentFrame = inParentFrame;
+ _track = inTrack;
+ }
+
+
+ /**
+ * Show the dialog to select options and export file
+ */
+ public boolean showDialog()
+ {
+ boolean fileSaved = false;
+ Object description = JOptionPane.showInputDialog(_parentFrame,
+ I18nManager.getText("dialog.exportkml.text"),
+ I18nManager.getText("dialog.exportkml.title"),
+ JOptionPane.QUESTION_MESSAGE, null, null, "");
+ if (description != null)
+ {
+ // OK pressed, so choose output file
+ if (_fileChooser == null)
+ _fileChooser = new JFileChooser();
+ _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
+ _fileChooser.setFileFilter(new FileFilter() {
+ public boolean accept(File f)
+ {
+ return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".kml")));
+ }
+ public String getDescription()
+ {
+ return "KML files";
+ }
+ });
+ _fileChooser.setAcceptAllFileFilterUsed(false);
+ // Allow choose again if an existing file is selected
+ boolean chooseAgain = false;
+ do
+ {
+ chooseAgain = false;
+ if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
+ {
+ // OK pressed and file chosen
+ File file = _fileChooser.getSelectedFile();
+ if (!file.getName().toLowerCase().endsWith(".kml"))
+ {
+ file = new File(file.getAbsolutePath() + ".kml");
+ }
+ // Check if file exists - if so don't overwrite
+ if (file.exists())
+ {
+ JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.fileexists"),
+ I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+ chooseAgain = true;
+ }
+ else
+ {
+ if (exportFile(file, description.toString()))
+ {
+ fileSaved = true;
+ }
+ else
+ {
+ chooseAgain = true;
+ }
+ }
+ }
+ } while (chooseAgain);
+ }
+ return fileSaved;
+ }
+
+
+ /**
+ * Export the track data to the specified file with description
+ * @param inFile File object to save to
+ * @param inDescription description to use, if any
+ */
+ private boolean exportFile(File inFile, String inDescription)
+ {
+ try
+ {
+ FileWriter writer = new FileWriter(inFile);
+ writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
+ writer.write("\t<name>");
+ writer.write(inDescription);
+ writer.write("</name>\n");
+
+ int i = 0;
+ DataPoint point = null;
+ boolean hasTrackpoints = false;
+ // Loop over waypoints
+ int numPoints = _track.getNumPoints();
+ for (i=0; i<numPoints; i++)
+ {
+ point = _track.getPoint(i);
+ if (point.isWaypoint())
+ {
+ exportWaypoint(point, writer);
+ }
+ else
+ {
+ hasTrackpoints = true;
+ }
+ }
+ if (hasTrackpoints)
+ {
+ writer.write("\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+ + "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+ + "\t\t</Style>\n\t\t<LineString>\n\t\t\t<coordinates>");
+ // Loop over track points
+ for (i=0; i<numPoints; i++)
+ {
+ point = _track.getPoint(i);
+ if (!point.isWaypoint())
+ {
+ exportTrackpoint(point, writer);
+ }
+ }
+ writer.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
+ }
+ writer.write("</Folder>\n</kml>");
+ 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<Placemark>\n\t\t<name>");
+ inWriter.write(inPoint.getFieldValue(Field.WAYPT_NAME).trim());
+ inWriter.write("</name>\n");
+ inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
+ 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</coordinates>\n\t\t</Point>\n\t</Placemark>\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");
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Track;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a track compression\r
+ */\r
+public class UndoCompress implements UndoOperation\r
+{\r
+ private DataPoint[] _contents = null;\r
+ protected int _numPointsDeleted = -1;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inTrack track contents to copy\r
+ */\r
+ public UndoCompress(Track inTrack)\r
+ {\r
+ _contents = inTrack.cloneContents();\r
+ }\r
+\r
+\r
+ /**\r
+ * Set the number of points deleted\r
+ * (only known after attempted compression)\r
+ * @param inNum number of points deleted\r
+ */\r
+ public void setNumPointsDeleted(int inNum)\r
+ {\r
+ _numPointsDeleted = inNum;\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including parameters\r
+ */\r
+ public String getDescription()\r
+ {\r
+ String desc = I18nManager.getText("undo.compress");\r
+ if (_numPointsDeleted > 0)\r
+ desc = desc + " (" + _numPointsDeleted + ")";\r
+ return desc;\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // restore track to previous values\r
+ inTrackInfo.getTrack().replaceContents(_contents);\r
+ // clear selection\r
+ inTrackInfo.getSelection().clearAll();\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+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;
+ }
+}
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Field;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a delete of a single point\r
+ */\r
+public class UndoDeletePoint implements UndoOperation\r
+{\r
+ private int _pointIndex = -1;\r
+ private DataPoint _point = null;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inIndex index number of point within track\r
+ * @param inPoint data point\r
+ */\r
+ public UndoDeletePoint(int inIndex, DataPoint inPoint)\r
+ {\r
+ _pointIndex = inIndex;\r
+ _point = inPoint;\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including point name if any\r
+ */\r
+ public String getDescription()\r
+ {\r
+ String desc = I18nManager.getText("undo.deletepoint");\r
+ String pointName = _point.getFieldValue(Field.WAYPT_NAME);\r
+ if (pointName != null && !pointName.equals(""))\r
+ desc = desc + " " + pointName;\r
+ return desc;\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrack Track object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // restore point into track\r
+ if (!inTrackInfo.getTrack().insertPoint(_point, _pointIndex))\r
+ {\r
+ throw new UndoException(getDescription());\r
+ }\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a delete of a range of points\r
+ */\r
+public class UndoDeleteRange implements UndoOperation\r
+{\r
+ private int _startIndex = -1;\r
+ private DataPoint[] _points = null;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inIndex index number of point within track\r
+ * @param inPoint data point\r
+ */\r
+ public UndoDeleteRange(TrackInfo inTrackInfo)\r
+ {\r
+ _startIndex = inTrackInfo.getSelection().getStart();\r
+ _points = inTrackInfo.cloneSelectedRange();\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including range length\r
+ */\r
+ public String getDescription()\r
+ {\r
+ return I18nManager.getText("undo.deleterange")\r
+ + " (" + _points.length + ")";\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo)\r
+ {\r
+ // restore point array into track\r
+ inTrackInfo.getTrack().insertRange(_points, _startIndex);\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+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);
+ }
+}
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo an insertion (eg interpolate)\r
+ */\r
+public class UndoInsert implements UndoOperation\r
+{\r
+ private int _startPosition = 0;\r
+ private int _numInserted = 0;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inStart start of insert\r
+ * @param inNumInserted number of points inserted\r
+ */\r
+ public UndoInsert(int inStart, int inNumInserted)\r
+ {\r
+ _startPosition = inStart;\r
+ _numInserted = inNumInserted;\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including parameters\r
+ */\r
+ public String getDescription()\r
+ {\r
+ return I18nManager.getText("undo.insert") + " (" + _numInserted + ")";\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given TrackInfo\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // restore track to previous values\r
+ inTrackInfo.getTrack().deleteRange(_startPosition, _startPosition + _numInserted - 1);\r
+ // reset selection\r
+ inTrackInfo.getSelection().select(_startPosition-1, _startPosition-1, _startPosition);\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a load operation\r
+ */\r
+public class UndoLoad implements UndoOperation\r
+{\r
+ private int _cropIndex = -1;\r
+ private int _numLoaded = -1;\r
+ private DataPoint[] _contents = null;\r
+ private String _previousFilename = null;\r
+\r
+\r
+ /**\r
+ * Constructor for appending\r
+ * @param inIndex index number of crop point\r
+ * @param inNumLoaded number of points loaded\r
+ */\r
+ public UndoLoad(int inIndex, int inNumLoaded)\r
+ {\r
+ _cropIndex = inIndex;\r
+ _numLoaded = inNumLoaded;\r
+ _contents = null;\r
+ _previousFilename = null;\r
+ }\r
+\r
+\r
+ /**\r
+ * Constructor for replacing\r
+ * @param inOldTrack track being replaced\r
+ * @param inNumLoaded number of points loaded\r
+ */\r
+ public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded)\r
+ {\r
+ _cropIndex = -1;\r
+ _numLoaded = inNumLoaded;\r
+ _contents = inOldTrackInfo.getTrack().cloneContents();\r
+ if (inOldTrackInfo.getFileInfo().getNumFiles() == 1)\r
+ _previousFilename = inOldTrackInfo.getFileInfo().getFilename();\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including number of points loaded\r
+ */\r
+ public String getDescription()\r
+ {\r
+ String desc = I18nManager.getText("undo.load");\r
+ if (_numLoaded > 0)\r
+ desc = desc + " (" + _numLoaded + ")";\r
+ return desc;\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // remove file from fileinfo\r
+ inTrackInfo.getFileInfo().removeFile();\r
+ if (_previousFilename != null)\r
+ {\r
+ inTrackInfo.getFileInfo().setFile(_previousFilename);\r
+ }\r
+ // Crop / replace\r
+ if (_contents == null)\r
+ {\r
+ // crop track to previous size\r
+ inTrackInfo.getTrack().cropTo(_cropIndex);\r
+ }\r
+ else\r
+ {\r
+ // replace track contents with old\r
+ if (!inTrackInfo.getTrack().replaceContents(_contents))\r
+ {\r
+ throw new UndoException(getDescription());\r
+ }\r
+ }\r
+ // clear selection\r
+ inTrackInfo.getSelection().clearAll();\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Interface implemented by all Undo Operations\r
+ */\r
+public interface UndoOperation\r
+{\r
+ /**\r
+ * Get the description of this operation\r
+ * @return description of operation including parameters\r
+ */\r
+ public String getDescription();\r
+\r
+ /**\r
+ * Perform the undo operation on the specified track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException;\r
+}
\ No newline at end of file
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Track;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo a waypoint rearrangement\r
+ */\r
+public class UndoRearrangeWaypoints implements UndoOperation\r
+{\r
+ private DataPoint[] _contents = null;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inTrack track contents to copy\r
+ */\r
+ public UndoRearrangeWaypoints(Track inTrack)\r
+ {\r
+ _contents = inTrack.cloneContents();\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation\r
+ */\r
+ public String getDescription()\r
+ {\r
+ return I18nManager.getText("undo.rearrangewaypoints");\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // restore track to previous values\r
+ inTrackInfo.getTrack().replaceContents(_contents);\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+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());
+ }
+ }
+}