+++ /dev/null
-package tim.prune.function.estimate;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.FlowLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.AdjustmentEvent;
-import java.awt.event.AdjustmentListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-import java.util.ArrayList;
-
-import javax.swing.BorderFactory;
-import javax.swing.Box;
-import javax.swing.BoxLayout;
-import javax.swing.JButton;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JScrollBar;
-
-import tim.prune.App;
-import tim.prune.GenericFunction;
-import tim.prune.I18nManager;
-import tim.prune.config.Config;
-import tim.prune.data.DataPoint;
-import tim.prune.data.Distance;
-import tim.prune.data.RangeStats;
-import tim.prune.data.Track;
-import tim.prune.data.Unit;
-import tim.prune.data.UnitSetLibrary;
-import tim.prune.function.estimate.jama.Matrix;
-import tim.prune.gui.ProgressDialog;
-
-/**
- * Function to learn the estimation parameters from the current track
- */
-public class LearnParameters extends GenericFunction implements Runnable
-{
- /** Progress dialog */
- ProgressDialog _progress = null;
- /** Results dialog */
- JDialog _dialog = null;
- /** Calculated parameters */
- private ParametersPanel _calculatedParamPanel = null;
- private EstimationParameters _calculatedParams = null;
- /** Slider for weighted average */
- private JScrollBar _weightSlider = null;
- /** Label to describe position of slider */
- private JLabel _sliderDescLabel = null;
- /** Combined parameters */
- private ParametersPanel _combinedParamPanel = null;
- /** Combine button */
- private JButton _combineButton = null;
-
-
- /**
- * Inner class used to hold the results of the matrix solving
- */
- static class MatrixResults
- {
- public EstimationParameters _parameters = null;
- public double _averageErrorPc = 0.0; // percentage
- }
-
-
- /**
- * Constructor
- * @param inApp App object
- */
- public LearnParameters(App inApp)
- {
- super(inApp);
- }
-
- /** @return key for function name */
- public String getNameKey() {
- return "function.learnestimationparams";
- }
-
- /**
- * Begin the function
- */
- public void begin()
- {
- // Show progress bar
- if (_progress == null) {
- _progress = new ProgressDialog(_parentFrame, getNameKey());
- }
- _progress.show();
- // Start new thread for the calculations
- new Thread(this).start();
- }
-
- /**
- * Run method in separate thread
- */
- public void run()
- {
- _progress.setMaximum(100);
- // Go through the track and collect the range stats for each sample
- ArrayList<RangeStats> statsList = new ArrayList<RangeStats>(20);
- Track track = _app.getTrackInfo().getTrack();
- final int numPoints = track.getNumPoints();
- final int sampleSize = numPoints / 30;
- int prevStartIndex = -1;
- for (int i=0; i<30; i++)
- {
- int startIndex = i * sampleSize;
- RangeStats stats = getRangeStats(track, startIndex, startIndex + sampleSize, prevStartIndex);
- if (stats != null && stats.getMovingDistanceKilometres() > 1.0
- && !stats.getTimestampsIncomplete() && !stats.getTimestampsOutOfSequence()
- && stats.getTotalDurationInSeconds() > 100
- && stats.getStartIndex() > prevStartIndex)
- {
- // System.out.println("Got stats for " + stats.getStartIndex() + " to " + stats.getEndIndex());
- statsList.add(stats);
- prevStartIndex = stats.getStartIndex();
- }
- _progress.setValue(i);
- }
-
- // Check if we've got enough samples
- // System.out.println("Got a total of " + statsList.size() + " samples");
- if (statsList.size() < 10)
- {
- _progress.dispose();
- // Show error message, not enough samples
- _app.showErrorMessage(getNameKey(), "error.learnestimationparams.failed");
- return;
- }
- // Loop around, solving the matrices and removing the highest-error sample
- MatrixResults results = reduceSamples(statsList);
- if (results == null)
- {
- _progress.dispose();
- _app.showErrorMessage(getNameKey(), "error.learnestimationparams.failed");
- return;
- }
-
- _progress.dispose();
-
- // Create the dialog if necessary
- if (_dialog == null)
- {
- _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
- _dialog.setLocationRelativeTo(_parentFrame);
- // Create Gui and show it
- _dialog.getContentPane().add(makeDialogComponents());
- _dialog.pack();
- }
-
- // Populate the values in the dialog
- populateCalculatedValues(results);
- updateCombinedLabels(calculateCombinedParameters());
- _dialog.setVisible(true);
- }
-
-
- /**
- * Make the dialog components
- * @return the GUI components for the dialog
- */
- private Component makeDialogComponents()
- {
- JPanel dialogPanel = new JPanel();
- dialogPanel.setLayout(new BorderLayout());
-
- // main panel with a box layout
- JPanel mainPanel = new JPanel();
- mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
- // Label at top
- JLabel introLabel = new JLabel(I18nManager.getText("dialog.learnestimationparams.intro") + ":");
- introLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
- introLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
- mainPanel.add(introLabel);
-
- // Panel for the calculated results
- _calculatedParamPanel = new ParametersPanel("dialog.estimatetime.results", true);
- _calculatedParamPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
- mainPanel.add(_calculatedParamPanel);
- mainPanel.add(Box.createVerticalStrut(14));
-
- mainPanel.add(new JLabel(I18nManager.getText("dialog.learnestimationparams.combine") + ":"));
- mainPanel.add(Box.createVerticalStrut(4));
- _weightSlider = new JScrollBar(JScrollBar.HORIZONTAL, 5, 1, 0, 11);
- _weightSlider.addAdjustmentListener(new AdjustmentListener() {
- public void adjustmentValueChanged(AdjustmentEvent inEvent)
- {
- if (!inEvent.getValueIsAdjusting()) {
- updateCombinedLabels(calculateCombinedParameters());
- }
- }
- });
- mainPanel.add(_weightSlider);
- _sliderDescLabel = new JLabel(" ");
- _sliderDescLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
- mainPanel.add(_sliderDescLabel);
- mainPanel.add(Box.createVerticalStrut(12));
-
- // Results panel
- _combinedParamPanel = new ParametersPanel("dialog.learnestimationparams.combinedresults");
- _combinedParamPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
- mainPanel.add(_combinedParamPanel);
-
- dialogPanel.add(mainPanel, BorderLayout.NORTH);
-
- // button panel at bottom
- JPanel buttonPanel = new JPanel();
- buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
-
- // Combine
- _combineButton = new JButton(I18nManager.getText("button.combine"));
- _combineButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent arg0) {
- combineAndFinish();
- }
- });
- buttonPanel.add(_combineButton);
-
- // Cancel
- JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
- cancelButton.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent e) {
- _dialog.dispose();
- }
- });
- KeyAdapter escapeListener = new KeyAdapter() {
- public void keyPressed(KeyEvent inE) {
- if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();}
- }
- };
- _combineButton.addKeyListener(escapeListener);
- cancelButton.addKeyListener(escapeListener);
- buttonPanel.add(cancelButton);
- dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
- return dialogPanel;
- }
-
- /**
- * Construct a rangestats object for the selected range
- * @param inTrack track object
- * @param inStartIndex start index
- * @param inEndIndex end index
- * @param inPreviousStartIndex the previously used start index, or -1
- * @return range stats object or null if required information missing from this bit of the track
- */
- private RangeStats getRangeStats(Track inTrack, int inStartIndex, int inEndIndex, int inPreviousStartIndex)
- {
- // Check parameters
- if (inTrack == null || inStartIndex < 0 || inEndIndex <= inStartIndex || inStartIndex > inTrack.getNumPoints()) {
- return null;
- }
- final int numPoints = inTrack.getNumPoints();
- int start = inStartIndex;
-
- // Search forward until a decent track point found for the start
- DataPoint p = inTrack.getPoint(start);
- while (start < numPoints && (p == null || p.isWaypoint() || !p.hasTimestamp() || !p.hasAltitude()))
- {
- start++;
- p = inTrack.getPoint(start);
- }
- if (inPreviousStartIndex >= 0 && start <= (inPreviousStartIndex + 10) // overlapping too much with previous range
- || (start >= (numPoints - 10))) // starting too late in the track
- {
- return null;
- }
-
- // Search forward (counting the radians) until a decent end point found
- double movingRads = 0.0;
- final double minimumRads = Distance.convertDistanceToRadians(1.0, UnitSetLibrary.UNITS_KILOMETRES);
- DataPoint prevPoint = inTrack.getPoint(start);
- int endIndex = start;
- boolean shouldStop = false;
- do
- {
- endIndex++;
- p = inTrack.getPoint(endIndex);
- if (p != null && !p.isWaypoint())
- {
- if (!p.hasAltitude() || !p.hasTimestamp()) {return null;} // abort if no time/altitude
- if (prevPoint != null && !p.getSegmentStart()) {
- movingRads += DataPoint.calculateRadiansBetween(prevPoint, p);
- }
- }
- prevPoint = p;
- if (endIndex >= numPoints) {
- shouldStop = true; // reached the end of the track
- }
- else if (movingRads >= minimumRads && endIndex >= inEndIndex) {
- shouldStop = true; // got at least a kilometre
- }
- }
- while (!shouldStop);
-
- // Check moving distance
- if (movingRads >= minimumRads) {
- return new RangeStats(inTrack, start, endIndex);
- }
- return null;
- }
-
- /**
- * Build an A matrix for the given list of RangeStats objects
- * @param inStatsList list of (non-null) RangeStats objects
- * @return A matrix with n rows and 5 columns
- */
- private static Matrix buildAMatrix(ArrayList<RangeStats> inStatsList)
- {
- final Unit METRES = UnitSetLibrary.UNITS_METRES;
- Matrix result = new Matrix(inStatsList.size(), 5);
- int row = 0;
- for (RangeStats stats : inStatsList)
- {
- result.setValue(row, 0, stats.getMovingDistanceKilometres());
- result.setValue(row, 1, stats.getGentleAltitudeRange().getClimb(METRES));
- result.setValue(row, 2, stats.getSteepAltitudeRange().getClimb(METRES));
- result.setValue(row, 3, stats.getGentleAltitudeRange().getDescent(METRES));
- result.setValue(row, 4, stats.getSteepAltitudeRange().getDescent(METRES));
- row++;
- }
- return result;
- }
-
- /**
- * Build a B matrix containing the observations (moving times)
- * @param inStatsList list of (non-null) RangeStats objects
- * @return B matrix with single column of n rows
- */
- private static Matrix buildBMatrix(ArrayList<RangeStats> inStatsList)
- {
- Matrix result = new Matrix(inStatsList.size(), 1);
- int row = 0;
- for (RangeStats stats : inStatsList)
- {
- result.setValue(row, 0, stats.getMovingDurationInSeconds() / 60.0); // convert seconds to minutes
- row++;
- }
- return result;
- }
-
- /**
- * Look for the maximum absolute value in the given column matrix
- * @param inMatrix matrix with only one column
- * @return row index of cell with greatest absolute value, or -1 if not valid
- */
- private static int getIndexOfMaxValue(Matrix inMatrix)
- {
- if (inMatrix == null || inMatrix.getNumColumns() > 1) {
- return -1;
- }
- int index = 0;
- double currValue = 0.0, maxValue = 0.0;
- // Loop over the first column looking for the maximum absolute value
- for (int i=0; i<inMatrix.getNumRows(); i++)
- {
- currValue = Math.abs(inMatrix.get(i, 0));
- if (currValue > maxValue)
- {
- maxValue = currValue;
- index = i;
- }
- }
- return index;
- }
-
- /**
- * See if the given set of samples is sufficient for getting a descent solution (at least 3 nonzero values)
- * @param inRangeSet list of RangeStats objects
- * @param inRowToIgnore row index to ignore, or -1 to use them all
- * @return true if the samples look ok
- */
- private static boolean isRangeSetSufficient(ArrayList<RangeStats> inRangeSet, int inRowToIgnore)
- {
- int numGC = 0, numSC = 0, numGD = 0, numSD = 0; // number of samples with gentle/steep climb/descent values > 0
- final Unit METRES = UnitSetLibrary.UNITS_METRES;
- int i = 0;
- for (RangeStats stats : inRangeSet)
- {
- if (i != inRowToIgnore)
- {
- if (stats.getGentleAltitudeRange().getClimb(METRES) > 0) {numGC++;}
- if (stats.getSteepAltitudeRange().getClimb(METRES) > 0) {numSC++;}
- if (stats.getGentleAltitudeRange().getDescent(METRES) > 0) {numGD++;}
- if (stats.getSteepAltitudeRange().getDescent(METRES) > 0) {numSD++;}
- }
- i++;
- }
- return numGC > 3 && numSC > 3 && numGD > 3 && numSD > 3;
- }
-
- /**
- * Reduce the number of samples in the given list by eliminating the ones with highest errors
- * @param inStatsList list of stats
- * @return results in an object
- */
- private MatrixResults reduceSamples(ArrayList<RangeStats> inStatsList)
- {
- int statsIndexToRemove = -1;
- Matrix answer = null;
- boolean finished = false;
- double averageErrorPc = 0.0;
- while (!finished)
- {
- // Remove the marked stats object, if any
- if (statsIndexToRemove >= 0) {
- inStatsList.remove(statsIndexToRemove);
- }
-
- // Build up the matrices
- Matrix A = buildAMatrix(inStatsList);
- Matrix B = buildBMatrix(inStatsList);
- // System.out.println("Times in minutes are:\n" + B.toString());
-
- // Solve (if possible)
- try
- {
- answer = A.solve(B);
- // System.out.println("Solved matrix with " + A.getNumRows() + " rows:\n" + answer.toString());
- // Work out the percentage error for each estimate
- Matrix estimates = A.times(answer);
- Matrix errors = estimates.minus(B).divideEach(B);
- // System.out.println("Errors: " + errors.toString());
- averageErrorPc = errors.getAverageAbsValue();
- // find biggest percentage error, remove it from list
- statsIndexToRemove = getIndexOfMaxValue(errors);
- if (statsIndexToRemove < 0)
- {
- System.err.println("Something wrong - index is " + statsIndexToRemove);
- throw new Exception();
- }
- // Check whether removing this element would make the range set insufficient
- finished = inStatsList.size() <= 25 || !isRangeSetSufficient(inStatsList, statsIndexToRemove);
- }
- catch (Exception e)
- {
- // Couldn't solve at all
- System.out.println("Failed to reduce: " + e.getClass().getName() + " - " + e.getMessage());
- return null;
- }
- _progress.setValue(20 + 80 * (30 - inStatsList.size())/5); // Counting from 30 to 25
- }
- // Copy results to an EstimationParameters object
- MatrixResults result = new MatrixResults();
- result._parameters = new EstimationParameters();
- result._parameters.populateWithMetrics(answer.get(0, 0) * 5, // convert from 1km to 5km
- answer.get(1, 0) * 100.0, answer.get(2, 0) * 100.0, // convert from m to 100m
- answer.get(3, 0) * 100.0, answer.get(4, 0) * 100.0);
- result._averageErrorPc = averageErrorPc;
- return result;
- }
-
-
- /**
- * Populate the dialog's labels with the calculated values
- * @param inResults results of the calculations
- */
- private void populateCalculatedValues(MatrixResults inResults)
- {
- if (inResults == null || inResults._parameters == null)
- {
- _calculatedParams = null;
- _calculatedParamPanel.updateParameters(null, 0.0);
- }
- else
- {
- _calculatedParams = inResults._parameters;
- _calculatedParamPanel.updateParameters(_calculatedParams, inResults._averageErrorPc);
- }
- }
-
- /**
- * Combine the calculated parameters with the existing ones
- * according to the value of the slider
- * @return combined parameters
- */
- private EstimationParameters calculateCombinedParameters()
- {
- final double fraction1 = 1 - 0.1 * _weightSlider.getValue(); // slider left = value 0 = fraction 1 = keep current
- EstimationParameters oldParams = new EstimationParameters(Config.getConfigString(Config.KEY_ESTIMATION_PARAMS));
- return oldParams.combine(_calculatedParams, fraction1);
- }
-
- /**
- * Update the labels to show the combined parameters
- * @param inCombinedParams combined estimation parameters
- */
- private void updateCombinedLabels(EstimationParameters inCombinedParams)
- {
- // Update the slider description label
- String sliderDesc = null;
- final int sliderVal = _weightSlider.getValue();
- switch (sliderVal)
- {
- case 0: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.100pccurrent"); break;
- case 5: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.50pc"); break;
- case 10: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.100pccalculated"); break;
- default:
- final int currTenths = 10 - sliderVal, calcTenths = sliderVal;
- sliderDesc = "" + currTenths + "0% " + I18nManager.getText("dialog.learnestimationparams.weight.current")
- + " + " + calcTenths + "0% " + I18nManager.getText("dialog.learnestimationparams.weight.calculated");
- }
- _sliderDescLabel.setText(sliderDesc);
- // And update all the combined params labels
- _combinedParamPanel.updateParameters(inCombinedParams);
- _combineButton.setEnabled(sliderVal > 0);
- }
-
- /**
- * React to the combine button, by saving the combined parameters in the config
- */
- private void combineAndFinish()
- {
- EstimationParameters params = calculateCombinedParameters();
- Config.setConfigString(Config.KEY_ESTIMATION_PARAMS, params.toConfigString());
- _dialog.dispose();
- }
-}