From 7dc9c28c7405233e8719cafd21ae7d25f81ebcf2 Mon Sep 17 00:00:00 2001 From: Vebryn Date: Sun, 14 May 2017 23:09:27 +0200 Subject: [PATCH] initial version of speed zones tab (#674) * #673 initial version of speed zones tab * #673 fix copyrights and initial step speed length --- .../activities/charts/ActivityAnalysis.java | 62 ++++- .../activities/charts/ChartsActivity.java | 6 +- .../activities/charts/StatsChartFragment.java | 217 ++++++++++++++++++ .../charts/XAxisValueFormatter.java | 42 ++++ .../main/res/layout/fragment_statschart.xml | 39 ++++ app/src/main/res/values/strings.xml | 4 + 6 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StatsChartFragment.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/XAxisValueFormatter.java create mode 100644 app/src/main/res/layout/fragment_statschart.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index acab06fb..d5ef1535 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -17,7 +17,9 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; +import java.util.HashMap; import java.util.List; +import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; @@ -25,6 +27,20 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; class ActivityAnalysis { + // store raw steps and duration + protected HashMap stats = new HashMap(); + // normalize steps + protected HashMap statsQuantified = new HashMap(); + // store maxSpeed / resolution + protected float maxSpeedQuantifier; + // store an average of round precision + protected float roundPrecision = 0f; + + // max speed determined from samples + private int maxSpeed = 0; + // number of bars on stats chart + private int resolution = 5; + ActivityAmounts calculateActivityAmounts(List samples) { ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP); ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP); @@ -53,7 +69,7 @@ class ActivityAnalysis { int steps = sample.getSteps(); if (steps > 0) { - amount.addSteps(sample.getSteps()); + amount.addSteps(steps); } if (previousSample != null) { @@ -65,12 +81,56 @@ class ActivityAnalysis { previousAmount.addSeconds(sharedTimeDifference); amount.addSeconds(sharedTimeDifference); } + + // add time + if (steps > 0 && sample.getKind() == ActivityKind.TYPE_ACTIVITY) { + if (steps > maxSpeed) { + maxSpeed = steps; + } + + if (!stats.containsKey(steps)) { + //System.out.println("Adding: " + steps); + stats.put(steps, timeDifference); + } else { + long time = stats.get(steps); + //System.out.println("Updating: " + steps + " " + timeDifference + time); + stats.put(steps, timeDifference + time); + } + } } previousAmount = amount; previousSample = sample; } + maxSpeedQuantifier = maxSpeed / resolution; + for (Map.Entry entry : stats.entrySet()) { + // 0.1 precision + //float keyQuantified = Math.round(entry.getKey() / maxSpeedQuantifier * 10f) / 10f; + + // 1 precision + float keyQuantified = entry.getKey() / maxSpeedQuantifier; + float keyQuantifiedRounded = Math.round(entry.getKey() / maxSpeedQuantifier); + float keyQuantifiedPrecision = keyQuantifiedRounded - keyQuantified; + roundPrecision = (roundPrecision + Math.abs(keyQuantifiedPrecision)) / 2; + //System.out.println("Precision: " + roundPrecision); + + // no scale + //keyQuantified = entry.getKey(); + + // scaling to minutes + float timeMinutes = entry.getValue() / 60; + + if (!statsQuantified.containsKey(keyQuantifiedRounded)) { + //System.out.println("Adding: " + keyQuantified + "/" + timeMinutes); + statsQuantified.put(keyQuantifiedRounded, timeMinutes); + } else { + float previousTime = statsQuantified.get(keyQuantifiedRounded); + //System.out.println("Updating: " + keyQuantified + "/" + (timeMinutes + previousTime)); + statsQuantified.put(keyQuantifiedRounded, (timeMinutes + previousTime)); + } + } + ActivityAmounts result = new ActivityAmounts(); if (deepSleep.getTotalSeconds() > 0) { result.addAmount(deepSleep); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java index 2b73b192..fae68b91 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java @@ -333,6 +333,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts return new WeekStepsChartFragment(); case 4: return new LiveActivityFragment(); + case 5: + return new StatsChartFragment(); } return null; @@ -341,7 +343,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts @Override public int getCount() { // Show 5 total pages. - return 5; + return 6; } @Override @@ -357,6 +359,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts return getString(R.string.weekstepschart_steps_a_week); case 4: return getString(R.string.liveactivity_live_activity); + case 5: + return getString(R.string.stats_title); } return super.getPageTitle(position); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StatsChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StatsChartFragment.java new file mode 100644 index 00000000..146f0629 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StatsChartFragment.java @@ -0,0 +1,217 @@ +/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer, + Daniele Gobbetti, Vebryn + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.components.LegendEntry; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.utils.ViewPortHandler; +import com.github.mikephil.charting.charts.HorizontalBarChart; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + + +public class StatsChartFragment extends AbstractChartFragment { + protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class); + + private HorizontalBarChart mStatsChart; + + private int mSmartAlarmFrom = -1; + private int mSmartAlarmTo = -1; + private int mTimestampFrom = -1; + private int mSmartAlarmGoneOff = -1; + + @Override + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + List samples = getSamples(db, device); + + MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples); + DefaultChartsData chartsData = refresh(device, samples); + + return new MyChartsData(mySleepChartsData, chartsData); + } + + private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List samples) { + ActivityAnalysis analysis = new ActivityAnalysis(); + analysis.calculateActivityAmounts(samples); + BarData data = new BarData(); + List entries = new ArrayList<>(); + XAxisValueFormatter customXAxis = new XAxisValueFormatter(); + + for (Map.Entry entry : analysis.statsQuantified.entrySet()) { + entries.add(new BarEntry(entry.getKey(), entry.getValue())); + /*float realValue = entry.getKey() * analysis.maxSpeedQuantifier; + String customLabel = Math.round(realValue * (1 - analysis.roundPrecision) * 10f) / 10f + " - " + Math.round(realValue * (1 + analysis.roundPrecision) * 10f) / 10f;*/ + customXAxis.add("" + entry.getKey() * analysis.maxSpeedQuantifier); + } + + BarDataSet set = new BarDataSet(entries, ""); + set.setColors(getColorFor(ActivityKind.TYPE_ACTIVITY)); + //set.setDrawValues(false); + //data.setBarWidth(0.1f); + data.addDataSet(set); + + // set X axis + customXAxis.sort(); + XAxis left = mStatsChart.getXAxis(); + left.setValueFormatter(customXAxis); + + // display precision + //mStatsChart.getDescription().setText(Math.round(analysis.roundPrecision * 100) + "%"); + + return new MySleepChartsData("", data); + } + + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + MyChartsData mcd = (MyChartsData) chartsData; + mStatsChart.setData(mcd.getPieData().getPieData()); + } + + @Override + public String getTitle() { + return getString(R.string.stats_title); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_statschart, container, false); + + mStatsChart = (HorizontalBarChart) rootView.findViewById(R.id.statschart); + setupStatsChart(); + + // refresh immediately instead of use refreshIfVisible(), for perceived performance + refresh(); + + return rootView; + } + + private void setupStatsChart() { + mStatsChart.setBackgroundColor(BACKGROUND_COLOR); + mStatsChart.getDescription().setTextColor(DESCRIPTION_COLOR); + mStatsChart.setNoDataText(""); + mStatsChart.getLegend().setEnabled(false); + mStatsChart.setTouchEnabled(false); + mStatsChart.getDescription().setText(""); + } + + @Override + protected void setupLegend(Chart chart) { + List legendEntries = new ArrayList<>(3); + LegendEntry lightSleepEntry = new LegendEntry(); + lightSleepEntry.label = akLightSleep.label; + lightSleepEntry.formColor = akLightSleep.color; + legendEntries.add(lightSleepEntry); + + LegendEntry deepSleepEntry = new LegendEntry(); + deepSleepEntry.label = akDeepSleep.label; + deepSleepEntry.formColor = akDeepSleep.color; + legendEntries.add(deepSleepEntry); + + if (supportsHeartrate(getChartsHost().getDevice())) { + LegendEntry hrEntry = new LegendEntry(); + hrEntry.label = HEARTRATE_LABEL; + hrEntry.formColor = HEARTRATE_COLOR; + legendEntries.add(hrEntry); + } + chart.getLegend().setCustom(legendEntries); + chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + } + + @Override + protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { +// temporary fix for totally wrong sleep amounts +// return super.getSleepSamples(db, device, tsFrom, tsTo); + return super.getAllSamples(db, device, tsFrom, tsTo); + } + + @Override + protected void renderCharts() { + mStatsChart.invalidate(); + } + + private static class MySleepChartsData extends ChartsData { + private String totalSleep; + private final BarData pieData; + + public MySleepChartsData(String totalSleep, BarData pieData) { + this.totalSleep = totalSleep; + this.pieData = pieData; + } + + public BarData getPieData() { + return pieData; + } + + public CharSequence getTotalSleep() { + return totalSleep; + } + } + + private static class MyChartsData extends ChartsData { + private final DefaultChartsData chartsData; + private final MySleepChartsData pieData; + + public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) { + this.pieData = pieData; + this.chartsData = chartsData; + } + + public MySleepChartsData getPieData() { + return pieData; + } + + public DefaultChartsData getChartsData() { + return chartsData; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/XAxisValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/XAxisValueFormatter.java new file mode 100644 index 00000000..62bce3cb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/XAxisValueFormatter.java @@ -0,0 +1,42 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by nhu on 30/04/17. + */ + +public class XAxisValueFormatter implements IAxisValueFormatter { + private List mValues = new ArrayList<>(); + + public XAxisValueFormatter() { + super(); + } + + public void add(String label) { + mValues.add(label); + } + + public void sort() { + //System.out.println("Sorting " + mValues); + Collections.sort(mValues); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { + String returnString = "N/A"; + + try { + returnString = mValues.get((int) value).toString(); + //System.out.println("Asking " + value + ", returning " + returnString); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return returnString; + } +} diff --git a/app/src/main/res/layout/fragment_statschart.xml b/app/src/main/res/layout/fragment_statschart.xml new file mode 100644 index 00000000..b6f5279e --- /dev/null +++ b/app/src/main/res/layout/fragment_statschart.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7ef2fe32..6bcba5bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -258,6 +258,10 @@ Navigation Social Network + Speed zones + Total minutes + Steps per minute + Find lost Device Cancel to stop vibration. Your Activity