/* * Copyright (C) 2012 - 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.util; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.Toast; import com.jjoe64.graphview.DefaultLabelFormatter; import com.jjoe64.graphview.GraphView; import com.jjoe64.graphview.series.DataPoint; import com.jjoe64.graphview.series.DataPointInterface; import com.jjoe64.graphview.series.LineGraphSeries; import com.jjoe64.graphview.series.OnDataPointTapListener; import com.jjoe64.graphview.series.Series; import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.db.entities.LocationEntity; import org.runnerup.view.HRZonesBar; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class GraphWrapper implements Constants { private GraphView graphView; private GraphView graphView2; private final LinearLayout graphTab; private final HRZonesBar hrzonesBar; private final Formatter formatter; private final LinearLayout hrzonesBarLayout; /** * Called when the activity is first created. */ public GraphWrapper(Context context, LinearLayout graphTab, LinearLayout hrzonesBarLayout, final Formatter formatter, SQLiteDatabase mDB, long mID) { this.graphTab = graphTab; this.hrzonesBarLayout = hrzonesBarLayout; this.formatter = formatter; if (Build.VERSION.SDK_INT > 8) { new LoadGraph().execute(new LoadParam(context, mDB, mID)); graphView = new GraphView(context); graphView.setTitle(context.getString(R.string.Pace)); graphView.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() { @Override public String formatLabel(double value, boolean isValueX) { if (isValueX) { return formatter.formatDistance(Formatter.Format.TXT, (long) value); } else { return formatter.formatPace(Formatter.Format.TXT_SHORT, value); } } }); graphView.getGridLabelRenderer().setVerticalAxisTitle(formatter.getPaceUnit()); graphView.getGridLabelRenderer().setHorizontalAxisTitle(formatter.getDistanceUnit(Formatter.Format.TXT)); //enable zoom graphView.getViewport().setScalable(true); graphView.getViewport().setScrollable(true); graphView2 = new GraphView(context); graphView2.setTitle(context.getString(R.string.Heart_rate)); graphView2.getGridLabelRenderer().setVerticalAxisTitle("bpm"); graphView2.getGridLabelRenderer().setHorizontalAxisTitle(formatter.getDistanceUnit(Formatter.Format.TXT)); graphView2.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() { @Override public String formatLabel(double value, boolean isValueX) { if (isValueX) { return formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) value); } else { return formatter.formatHeartRate(Formatter.Format.TXT_SHORT, value); } } }); graphView2.getViewport().setScalable(true); graphView2.getViewport().setScrollable(true); } hrzonesBar = new HRZonesBar(context); } class GraphProducer { final int interval; boolean first = true; int pos = 0; double time[] = null; double distance[] = null; double sum_time = 0; double sum_distance = 0; double acc_time = 0; int[] hr = null; double[] hrzHist = null; double tot_avg_hr = 0; double avg_pace = 0; double min_pace = Double.MAX_VALUE; double max_pace = Double.MIN_VALUE; List<DataPoint> paceList = null; List<DataPoint> hrList = null; boolean showHR = false; boolean showHRZhist = false; HRZones hrCalc = null; public GraphProducer(Context context, int noPoints) { final int GRAPH_INTERVAL_SECONDS = 5; // 1 point every 5 sec final int GRAPH_AVERAGE_SECONDS = 30; // moving average 30 sec final int graphAverageSeconds; if (noPoints < 60) { //This is short, maybe when testing. Dont bother to check time between points graphAverageSeconds = 1; this.interval = 2; } else { graphAverageSeconds = GRAPH_AVERAGE_SECONDS; this.interval = GRAPH_INTERVAL_SECONDS; } this.paceList = new ArrayList<>(); this.time = new double[graphAverageSeconds]; this.distance = new double[graphAverageSeconds]; this.hrList = new ArrayList<>(); this.hr = new int[graphAverageSeconds]; Resources res = context.getResources(); Context ctx = context.getApplicationContext(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); this.hrCalc = new HRZones(res, prefs); if (hrCalc.isConfigured()) { this.hrzHist = new double[hrCalc.getCount() + 1]; for (int i = 0; i < this.hrzHist.length; i++) { this.hrzHist[i] = 0; } showHRZhist = true; } clearSmooth(0); } void clearSmooth(double tot_distance) { if (pos >= (this.time.length / 2) && (acc_time >= 1000 * (interval / 2)) && sum_distance > 0) { emit(tot_distance); } for (int i = 0; i < this.distance.length; i++) { time[i] = 0; distance[i] = 0; hr[i] = 0; } pos = 0; sum_time = 0; sum_distance = 0; acc_time = 0; } void addObservation(double delta_time, double delta_distance, double tot_distance, LocationEntity loc) { if (delta_time < 500) return; //Update moving average int p = pos % this.time.length; sum_time -= this.time[p]; sum_distance -= this.distance[p]; sum_time += delta_time; sum_distance += delta_distance; this.time[p] = delta_time; this.distance[p] = delta_distance; if (loc.getHr() != null) { showHR = true; int hr = loc.getHr(); this.hr[p] = hr; if (showHRZhist && hr > 0) { this.hrzHist[hrCalc.getZoneInt(hr)] += delta_time; } } else { this.hr[p] = 0; } pos += 1; acc_time += delta_time; if (pos >= this.time.length && (acc_time >= 1000 * interval) && sum_distance > 0) { emit(tot_distance); } } void emit(double tot_distance) { double avg_time = sum_time; double avg_dist = sum_distance; double avg_hr = calculateAverageHr(hr); { double max_pace[] = { 0, 0, 0 }; double min_pace[] = { Double.MAX_VALUE, 0, 0 }; for (int i = 0; i < this.time.length; i++) { double pace = this.time[i] / this.distance[i]; if (pace > max_pace[0]) { max_pace[0] = pace; max_pace[1] = this.time[i]; max_pace[2] = this.distance[i]; } if (pace < min_pace[0]) { min_pace[0] = pace; min_pace[1] = this.time[i]; min_pace[2] = this.distance[i]; } } //avg_time -= (max_pace[1] + min_pace[1]); //avg_dist -= (max_pace[2] + min_pace[2]); } if (avg_dist > 0) { double pace = avg_time / avg_dist / 1000.0; if (first) { paceList.add(new DataPoint(0, pace)); hrList.add(new DataPoint(0, Math.round(avg_hr))); first = false; } paceList.add(new DataPoint(tot_distance, pace)); hrList.add(new DataPoint(tot_distance, Math.round(avg_hr))); acc_time = 0; tot_avg_hr += avg_hr; avg_pace += pace; min_pace = Math.min(min_pace, pace); max_pace = Math.max(max_pace, pace); } } class GraphFilter { double data[] = null; final List<DataPoint> source; GraphFilter(List<DataPoint> paceList) { source = paceList; data = new double[paceList.size()]; for (int i = 0; i < paceList.size(); i++) data[i] = paceList.get(i).getY(); } void complete() { for (int i = 0; i < source.size(); i++) source.set(i, new DataPoint(source.get(i).getX(), data[i])); } void init(double window[], double val) { for (int j = 0; j < window.length - 1; j++) window[j] = val; } void shiftLeft(double window[], double newVal) { System.arraycopy(window, 1, window, 0, window.length - 1); window[window.length - 1] = newVal; } /** * Perform in place moving average */ void movingAvergage(int windowLen) { double window[] = new double[windowLen]; init(window, data[0]); final int mid = (window.length - 1) / 2; final int last = window.length - 1; for (int i = 0; i < data.length && i <= mid; i++) { window[i + mid] = data[i]; } double sum = 0; for (double aWindow : window) sum += aWindow; for (int i = 0; i < data.length; i++) { double newY = sum / windowLen; data[i] = newY; sum -= window[0]; shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_pace); sum += window[last]; } } /** * Perform in place moving average */ void movingMedian(int windowLen) { double window[] = new double[windowLen]; init(window, data[0]); final int mid = (window.length - 1) / 2; for (int i = 0; i < data.length && i <= mid; i++) { window[i + mid] = data[i]; } double sort[] = new double[windowLen]; for (int i = 0; i < data.length; i++) { System.arraycopy(window, 0, sort, 0, windowLen); Arrays.sort(sort); data[i] = sort[mid]; shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_pace); } } /** * Perform in place SavitzkyGolay windowLen = 5 */ void SavitzkyGolay5() { final int len = 5; double window[] = new double[len]; init(window, data[0]); final int mid = (window.length - 1) / 2; for (int i = 0; i < data.length && i <= mid; i++) { window[i + mid] = data[i]; } for (int i = 0; i < data.length; i++) { double newY = (-3 * window[0] + 12 * window[1] + 17 * window[2] + 12 * window[3] - 3 * window[4]) / 35; data[i] = newY; shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_pace); } } /** * Perform in place SavitzkyGolay windowLen = 7 */ void SavitzkyGolay7() { final int len = 7; double window[] = new double[len]; init(window, data[0]); final int mid = (window.length - 1) / 2; for (int i = 0; i < data.length && i <= mid; i++) { window[i + mid] = data[i]; } for (int i = 0; i < data.length; i++) { double newY = (-2 * window[0] + 3 * window[1] + 6 * window[2] + 7 * window[3] + 6 * window[4] + 3 * window[5] - 2 * window[6]) / 21; data[i] = newY; shiftLeft(window, (i + mid) < data.length ? data[i + mid] : avg_pace); } } void KolmogorovZurbenko(int n, int len) { for (int i = 0; i < n; i++) movingAvergage(len); } } public void complete(final GraphView graphView) { avg_pace /= paceList.size(); Log.e(getClass().getName(), "graph: " + paceList.size() + " points"); boolean smoothData = PreferenceManager.getDefaultSharedPreferences(graphView.getContext()) .getBoolean(graphView.getContext().getResources().getString(R.string.pref_pace_graph_smoothing), true); if (paceList.size() > 0 && smoothData) { GraphFilter f = new GraphFilter(paceList); final String defaultFilterList = graphView.getContext().getResources().getString(R.string.mm31kz513sg5); final String filterList = PreferenceManager.getDefaultSharedPreferences( graphView.getContext()).getString( graphView.getContext().getResources().getString(R.string.pref_pace_graph_smoothing_filters), defaultFilterList); final String filters[] = filterList.split(";"); System.err.print("Applying filters(" + filters.length + ", >" + filterList + "<):"); for (String filter : filters) { int args[] = getArgs(filter); if (filter.startsWith("mm")) { if (args.length == 1) { f.movingMedian(args[0]); System.err.print(" mm(" + args[0] + ")"); } } else if (filter.startsWith("ma")) { if (args.length == 1) { f.movingAvergage(args[0]); System.err.print(" ma(" + args[0] + ")"); } } else if (filter.startsWith("kz")) { if (args.length == 2) { f.KolmogorovZurbenko(args[0], args[1]); System.err.print(" kz(" + args[0] + "," + args[1] + ")"); } } else if (filter.startsWith("sg")) { if (args.length == 1 && args[0] == 5) { f.SavitzkyGolay5(); System.err.print(" sg(5)"); } else if (args.length == 1 && args[0] == 7) { f.SavitzkyGolay7(); System.err.print(" sg(7)"); } } } Log.e(getClass().getName(), ""); f.complete(); } LineGraphSeries<DataPoint> graphViewData = new LineGraphSeries<>( paceList.toArray(new DataPoint[paceList.size()])); graphView.addSeries(graphViewData); // data graphView.getViewport().setMinX(graphView.getViewport().getMinX(true)); graphView.getViewport().setMaxX(graphView.getViewport().getMaxX(true)); graphViewData.setOnDataPointTapListener(new OnDataPointTapListener() { @Override public void onTap(Series series, DataPointInterface dataPoint) { String msg = graphView.getContext().getString(R.string.Distance) + ": " + formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) dataPoint.getX()) + "\n" + graphView.getContext().getString(R.string.Pace) + ": " + formatter.formatPace(Formatter.Format.TXT_SHORT, dataPoint.getY()); Toast.makeText(graphView.getContext(), msg, Toast.LENGTH_SHORT).show(); } }); if (showHR) { LineGraphSeries<DataPoint> graphViewData2 = new LineGraphSeries<>( hrList.toArray(new DataPoint[hrList.size()])); graphView2.addSeries(graphViewData2); // data graphView2.getViewport().setMinX(graphView2.getViewport().getMinX(true)); graphView2.getViewport().setMaxX(graphView2.getViewport().getMaxX(true)); graphViewData2.setOnDataPointTapListener(new OnDataPointTapListener() { @Override public void onTap(Series series, DataPointInterface dataPoint) { String msg = graphView.getContext().getString(R.string.Distance) + ": " + formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) dataPoint.getX()) + "\n" + graphView.getContext().getString(R.string.Heart_rate) + ": " + formatter.formatHeartRate(Formatter.Format.TXT_SHORT, dataPoint.getY()); Toast.makeText(graphView.getContext(), msg, Toast.LENGTH_SHORT).show(); } }); if (showHRZhist) { System.err.print("HR Zones:"); double sum = 0; for (double aHrzHist : hrzHist) { sum += aHrzHist; } for (int i = 0; i < hrzHist.length; i++) { hrzHist[i] = hrzHist[i] / sum; System.err.print(" " + hrzHist[i]); } Log.e(getClass().getName(), "\n"); hrzonesBar.pushHrzData(hrzHist); } } } private int[] getArgs(String s) { try { s = s.substring(s.indexOf('(') + 1); s = s.substring(0, s.indexOf(')')); String sargs[] = s.split(","); int args[] = new int[sargs.length]; for (int i = 0; i < args.length; i++) { args[i] = Integer.parseInt(sargs[i]); } return args; } catch (Exception e) { e.printStackTrace(); return new int[0]; } } public boolean HasHRInfo() { return showHR; } public boolean HasHRZHist() { return showHR && showHRZhist; } } private double calculateAverageHr(int[] data) { int sum = 0; int no = 0; for (int aData : data) { if (aData> 0) { sum = sum + aData; no++; } } //TODO Average of pointe, not over time if(no==0){ return 0; } else { return (double) sum / no; } } class LoadParam { public LoadParam(Context context, SQLiteDatabase mDB, long mID) { this.context = context; this.mDB = mDB; this.mID = mID; } final Context context; final SQLiteDatabase mDB; final long mID; } private class LoadGraph extends AsyncTask<LoadParam, Void, GraphProducer> { @Override protected GraphProducer doInBackground(LoadParam... params) { LocationEntity.LocationList<LocationEntity> ll = new LocationEntity.LocationList<>(params[0].mDB, params[0].mID); GraphProducer graphData = new GraphProducer(params[0].context, ll.getCount()); double lastDistance = 0; long lastTime = 0; int lastLap = -1; Double tot_distance = 0.0; for (LocationEntity loc : ll) { Long time = loc.getTime(); time = time != null ? time : lastTime; Integer lap = loc.getLap(); lap = lap != null ? lap : 0; tot_distance = loc.getDistance(); tot_distance = tot_distance != null ? tot_distance : lastDistance; if (lap != lastLap) { graphData.clearSmooth(tot_distance); lastLap = lap; } graphData.addObservation(time - lastTime, tot_distance - lastDistance, tot_distance, loc); lastTime = time; lastDistance = tot_distance; } graphData.clearSmooth(tot_distance); // Log.e(getClass().getName(), "Finished loading " + cnt + " points"); //} ll.close(); return graphData; } @Override protected void onPostExecute(GraphProducer graphData) { if (Build.VERSION.SDK_INT > 8) { graphData.complete(graphView); if (!graphData.HasHRInfo()) { graphTab.addView(graphView); } else { graphTab.addView(graphView, new LayoutParams( LayoutParams.MATCH_PARENT, 0, 0.5f)); graphTab.addView(graphView2, new LayoutParams( LayoutParams.MATCH_PARENT, 0, 0.5f)); } if (graphData.HasHRZHist()) { hrzonesBarLayout.setVisibility(View.VISIBLE); hrzonesBarLayout.addView(hrzonesBar); } else { hrzonesBarLayout.setVisibility(View.GONE); } } } } }