/* * Copyright (C) 2013 jonas.oreland@gmail.com * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package org.runnerup.workout; import android.annotation.TargetApi; import android.os.Build; import android.util.Log; import org.runnerup.BuildConfig; @TargetApi(Build.VERSION_CODES.FROYO) public class TargetTrigger extends Trigger { boolean inited = false; boolean paused = false; int graceCount = 30; // final int initialGrace = 20; int minGraceCount = 30; // Scope scope = Scope.STEP; Dimension dimension = Dimension.PACE; Range range = null; int cntMeasures = 0; double measure[] = null; int skip_values = 1; double sort_measure[] = null; double lastTimestamp = 0; double measure_time[] = null; double measure_distance[] = null; /** * cache computing of median */ double lastVal = 0; int lastValCnt = 0; public TargetTrigger(Dimension dim, int movingAverageSeconds, int graceSeconds) { dimension = dim; measure = new double[movingAverageSeconds]; sort_measure = new double[movingAverageSeconds]; if (dimension == Dimension.HRZ) dimension = Dimension.HR; measure_time = new double[movingAverageSeconds]; measure_distance = new double[movingAverageSeconds]; minGraceCount = graceSeconds; skip_values = (5 * movingAverageSeconds) / 100; // ignore 5% lowest and // 5% higest values reset(); } @Override public boolean onTick(Workout w) { if (paused) { return false; } if (!w.isEnabled(dimension, Scope.STEP)) { inited = false; return false; } double time_now = w.get(Scope.STEP, Dimension.TIME); if (time_now < lastTimestamp) { Log.i(getClass().getName(), "time_now < lastTimestamp"); reset(); return false; } if (inited == false) { Log.i(getClass().getName(), "inited == false"); lastTimestamp = time_now; initMeasurement(w, time_now); inited = true; return false; } if ((time_now - lastTimestamp) < 1.0) { return false; } final int elapsed_seconds = (int) (time_now - lastTimestamp); lastTimestamp = time_now; try { double val_now = getMeasurement(w, time_now); for (int i = 0; i < elapsed_seconds; i++) { addObservation(val_now); } // Log.e(getName(), "val_now: " + val_now + " elapsed: " + // elapsed_seconds); if (graceCount > 0) { // only emit coaching ever so often // Log.e(getName(), "graceCount: " + graceCount); graceCount -= elapsed_seconds; } else { double avg = getValue(); double cmp = range.compare(avg); // Log.e(getName(), " => avg: " + avg + " => cmp: " + cmp); if (cmp == 0) { return false; } fire(w); graceCount = minGraceCount; } } catch (ArithmeticException ex) { return false; } return false; } private void addObservation(double val_now) { int pos = cntMeasures % measure.length; measure[pos] = val_now; cntMeasures++; } public double getValue() { if (cntMeasures == lastValCnt) return lastVal; //not all values in the measure array are meaningful when //cntMeasures is small so we adjust for it. int meaningful_length = Math.min(cntMeasures,measure.length); //should the percentage of values skipped be a variable of the class? int meaningful_skip_values = (5*meaningful_length)/100; System.arraycopy(measure, 0, sort_measure, 0, meaningful_length); java.util.Arrays.sort(sort_measure,0,meaningful_length); double cnt = 0; double val = 0; for (int i = meaningful_skip_values; i < meaningful_length - meaningful_skip_values; i++) { val += sort_measure[i]; cnt++; } lastVal = val / cnt; lastValCnt = cntMeasures; return lastVal; } private void reset() { for (int i = 0; i < measure.length; i++) { measure[i] = 0; } inited = false; cntMeasures = 0; graceCount = initialGrace; lastTimestamp = 0; lastVal = 0; lastValCnt = 0; } private void initMeasurement(Workout w, double time_now) { switch (dimension) { case PACE: case SPEED: double distance_now = w.get(scope, Dimension.DISTANCE); if (lastTimestamp == 0) { measure_time[0] = time_now; measure_distance[0] = distance_now; } case DISTANCE: case HR: case HRZ: case CAD: case TEMPERATURE: case PRESSURE: case TIME: default: break; } } private double getMeasurement(Workout w, double time_now) { switch (dimension) { case PACE: case SPEED: double distance_now = w.get(scope, Dimension.DISTANCE); int oldpos = 0; int newpos = (cntMeasures + 1) % measure_time.length; if (cntMeasures >= measure_time.length) { oldpos = newpos; } double delta_distance = distance_now - measure_distance[oldpos]; double delta_time = time_now - measure_time[oldpos]; measure_time[newpos] = time_now; measure_distance[newpos] = distance_now; if (dimension == Dimension.PACE) { return delta_time / delta_distance; } else { if (BuildConfig.DEBUG && dimension != Dimension.SPEED) { throw new AssertionError(); } return delta_distance / delta_time; } case DISTANCE: case HR: case HRZ: case CAD: case TEMPERATURE: case PRESSURE: case TIME: default: break; } return w.get(Scope.CURRENT, dimension); } @Override public void onRepeat(int current, int limit) { } @Override public void onStart(Scope what, Workout s) { if (this.scope == what) { reset(); for (Feedback f : triggerAction) { f.onStart(s); } } } @Override public void onPause(Workout s) { paused = true; } @Override public void onStop(Workout s) { paused = true; } @Override public void onResume(Workout s) { paused = false; reset(); } @Override public void onComplete(Scope what, Workout s) { } }