File: LearnParameters.java

package info (click to toggle)
gpsprune 17-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,984 kB
  • ctags: 5,218
  • sloc: java: 39,403; sh: 25; makefile: 17; python: 15
file content (520 lines) | stat: -rw-r--r-- 17,218 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
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();
	}
}