diff --git a/android_app/app/src/main/java/com/health/openscale/core/utils/PolynomialFitter.java b/android_app/app/src/main/java/com/health/openscale/core/utils/PolynomialFitter.java new file mode 100644 index 00000000..eadb54af --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/utils/PolynomialFitter.java @@ -0,0 +1,215 @@ +package com.health.openscale.core.utils; + +/*************************************************************************** + * Copyright (C) 2009 by Paul Lutus, Ian Clarke * + * lutusp@arachnoid.com, ian.clarke@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 2 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, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +import java.util.ArrayList; +import java.util.List; + +/** + * A class to fit a polynomial to a (potentially very large) dataset. + * + * @author Paul Lutus + * @author Ian Clarke + * + */ + +/* + * Changelog: + * 20100130: Add note about relicensing + * 20091114: Modify so that points can be added after the curve is + * created, also some other minor fixes + * 20091113: Extensively modified by Ian Clarke, main changes: + * - Should now be able to handle extremely large datasets + * - Use generic Java collections classes and interfaces + * where possible + * - Data can be fed to the fitter as it is available, rather + * than all at once + * + * The code that this is based on was obtained from: http://arachnoid.com/polysolve + * + * Note: I (Ian Clarke) am happy to release this code under a more liberal + * license such as the LGPL, however Paul Lutus (the primary author) refuses + * to do this on the grounds that the LGPL is not an open source license. + * If you want to try to explain to him that the LGPL is indeed an open + * source license, good luck - it's like talking to a brick wall. + */ +public class PolynomialFitter { + + private final int p, rs; + + private long n = 0; + + private final double[][] m; + + private final double[] mpc; + /** + * @param degree + * The degree of the polynomial to be fit to the data + */ + public PolynomialFitter(final int degree) { + assert degree > 0; + p = degree + 1; + rs = 2 * p - 1; + m = new double[p][p + 1]; + mpc = new double[rs]; + } + + /** + * Add a point to the set of points that the polynomial must be fit to + * + * @param x + * The x coordinate of the point + * @param y + * The y coordinate of the point + */ + public void addPoint(final double x, final double y) { + assert !Double.isInfinite(x) && !Double.isNaN(x); + assert !Double.isInfinite(y) && !Double.isNaN(y); + n++; + // process precalculation array + for (int r = 1; r < rs; r++) { + mpc[r] += Math.pow(x, r); + } + // process RH column cells + m[0][p] += y; + for (int r = 1; r < p; r++) { + m[r][p] += Math.pow(x, r) * y; + } + } + + /** + * Returns a polynomial that seeks to minimize the square of the total + * distance between the set of points and the polynomial. + * + * @return A polynomial + */ + public Polynomial getBestFit() { + final double[] mpcClone = mpc.clone(); + final double[][] mClone = new double[m.length][]; + for (int x = 0; x < mClone.length; x++) { + mClone[x] = m[x].clone(); + } + + mpcClone[0] += n; + // populate square matrix section + for (int r = 0; r < p; r++) { + for (int c = 0; c < p; c++) { + mClone[r][c] = mpcClone[r + c]; + } + } + gj_echelonize(mClone); + final Polynomial result = new Polynomial(p); + for (int j = 0; j < p; j++) { + result.add(j, mClone[j][p]); + } + return result; + } + private double fx(final double x, final List terms) { + double a = 0; + int e = 0; + for (final double i : terms) { + a += i * Math.pow(x, e); + e++; + } + return a; + } + private void gj_divide(final double[][] A, final int i, final int j, final int m) { + for (int q = j + 1; q < m; q++) { + A[i][q] /= A[i][j]; + } + A[i][j] = 1; + } + + private void gj_echelonize(final double[][] A) { + final int n = A.length; + final int m = A[0].length; + int i = 0; + int j = 0; + while (i < n && j < m) { + // look for a non-zero entry in col j at or below row i + int k = i; + while (k < n && A[k][j] == 0) { + k++; + } + // if such an entry is found at row k + if (k < n) { + // if k is not i, then swap row i with row k + if (k != i) { + gj_swap(A, i, j); + } + // if A[i][j] is not 1, then divide row i by A[i][j] + if (A[i][j] != 1) { + gj_divide(A, i, j, m); + } + // eliminate all other non-zero entries from col j by + // subtracting from each + // row (other than i) an appropriate multiple of row i + gj_eliminate(A, i, j, n, m); + i++; + } + j++; + } + } + + private void gj_eliminate(final double[][] A, final int i, final int j, final int n, final int m) { + for (int k = 0; k < n; k++) { + if (k != i && A[k][j] != 0) { + for (int q = j + 1; q < m; q++) { + A[k][q] -= A[k][j] * A[i][q]; + } + A[k][j] = 0; + } + } + } + + private void gj_swap(final double[][] A, final int i, final int j) { + double temp[]; + temp = A[i]; + A[i] = A[j]; + A[j] = temp; + } + + + public static class Polynomial extends ArrayList { + private static final long serialVersionUID = 1692843494322684190L; + + public Polynomial(final int p) { + super(p); + } + + public double getY(final double x) { + double ret = 0; + for (int p=0; p -1; x--) { + ret.append(get(x) + (x > 0 ? "x^" + x + " + " : "")); + } + return ret.toString(); + } + } +} \ No newline at end of file diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java index 6b202b34..299d36a4 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java @@ -36,6 +36,7 @@ import android.widget.TextView; import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleData; +import com.health.openscale.core.utils.PolynomialFitter; import com.health.openscale.gui.activities.DataEntryActivity; import java.text.SimpleDateFormat; @@ -284,7 +285,6 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { setHasPoints(prefs.getBoolean("pointsEnable", true)). setFormatter(new SimpleLineChartValueFormatter(1)); - if(prefs.getBoolean("weightEnable", true) && prefs.getBoolean(String.valueOf(diagramWeight.getId()), true)) { lines.add(lineWeight); diagramWeight.setBackgroundTintList(ColorStateList.valueOf(ChartUtils.COLOR_VIOLET)); @@ -333,6 +333,22 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { enableMonth.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#d3d3d3"))); } + LineChartData lineData = new LineChartData(lines); + lineData.setAxisXBottom(new Axis(axisValues). + setHasLines(true). + setTextColor(Color.BLACK) + ); + + lineData.setAxisYLeft(new Axis(). + setHasLines(true). + setMaxLabelChars(5). + setTextColor(Color.BLACK) + ); + + chartBottom.setLineChartData(lineData); + + defaultTopViewport = new Viewport(0, chartBottom.getCurrentViewport().top+4, axisValues.size()-1, chartBottom.getCurrentViewport().bottom-4); + if (prefs.getBoolean("goalLine", true)) { Stack valuesGoalLine = new Stack(); @@ -349,112 +365,23 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { lines.add(goalLine); } - if (prefs.getBoolean("regressionLine", true)) { + if (prefs.getBoolean("regressionLine", false)) { + PolynomialFitter polyFitter = new PolynomialFitter(Integer.parseInt(prefs.getString("regressionLineOrder", "1"))); - /* - // quadratic regression y = ax^2 + bx + c - double x_value = 0.0; - double y_value = 0.0; - - double s40 = 0; //sum of x^4 - double s30 = 0; //sum of x^3 - double s20 = 0; //sum of x^2 - double s10 = 0; //sum of x - double s00 = scaleDataList.size(); - //sum of x^0 * y^0 ie 1 * number of entries - - double s21 = 0; //sum of x^2*y - double s11 = 0; //sum of x*y - double s01 = 0; //sum of y - - for(ScaleData scaleEntry: scaleDataList) { - calDB.setTime(scaleEntry.getDateTime()); - - x_value = calDB.get(field); - y_value = scaleEntry.getConvertedWeight(openScale.getSelectedScaleUser().scale_unit); - - s40 += Math.pow(x_value, 4); - s30 += Math.pow(x_value, 3); - s20 += Math.pow(x_value, 2); - s10 += x_value; - - s21 += Math.pow(x_value, 2) * y_value; - s11 += x_value * y_value; - s01 += y_value; + for(PointValue weightValue : valuesWeight) { + polyFitter.addPoint(weightValue.getX(), weightValue.getY()); } - // solve equations using Cramer's law - double a = (s21*(s20 * s00 - s10 * s10) - - s11*(s30 * s00 - s10 * s20) + - s01*(s30 * s10 - s20 * s20)) - / - (s40*(s20 * s00 - s10 * s10) - - s30*(s30 * s00 - s10 * s20) + - s20*(s30 * s10 - s20 * s20)); - - double b = (s40*(s11 * s00 - s01 * s10) - - s30*(s21 * s00 - s01 * s20) + - s20*(s21 * s10 - s11 * s20)) - / - (s40 * (s20 * s00 - s10 * s10) - - s30 * (s30 * s00 - s10 * s20) + - s20 * (s30 * s10 - s20 * s20)); - - double c = (s40*(s20 * s01 - s10 * s11) - - s30*(s30 * s01 - s10 * s21) + - s20*(s30 * s11 - s20 * s21)) - / - (s40 * (s20 * s00 - s10 * s10) - - s30 * (s30 * s00 - s10 * s20) + - s20 * (s30 * s10 - s20 * s20)); - */ - - // linear regression y = a + x*b - double sumx = 0.0; - double sumy = 0.0; - - double x_value = 0.0; - double y_value = 0.0; - - for(ScaleData scaleEntry: scaleDataList) { - calDB.setTime(scaleEntry.getDateTime()); - - x_value = calDB.get(field); - y_value = scaleEntry.getConvertedWeight(openScale.getSelectedScaleUser().scale_unit); - - sumx += x_value; - sumy += y_value; - } - - double xbar = sumx / scaleDataList.size(); - double ybar = sumy / scaleDataList.size(); - - double xxbar = 0.0; - double xybar = 0.0; - - for(ScaleData scaleEntry: scaleDataList) { - calDB.setTime(scaleEntry.getDateTime()); - - x_value = calDB.get(field); - y_value = scaleEntry.getConvertedWeight(openScale.getSelectedScaleUser().scale_unit); - - xxbar += (x_value - xbar) * (x_value - xbar); - xybar += (y_value - xbar) * (y_value - ybar); - } - - double b = xybar / xxbar; - double a = ybar - b * xbar; - + PolynomialFitter.Polynomial polynom = polyFitter.getBestFit(); Stack valuesLinearRegression = new Stack(); - for (int i = 0; i < 31; i++) { - y_value = a + b * i; // linear regression - //y_value = a * i*i + b * i + c; // quadratic regression - - valuesLinearRegression.push(new PointValue((float) i, (float) y_value)); + if (!valuesWeight.isEmpty()) { + for (int i = (int)valuesWeight.peek().getX(); i <= 31; i++) { + double y_value = polynom.getY(i); + valuesLinearRegression.push(new PointValue((float) i, (float) y_value)); + } } - Line linearRegressionLine = new Line(valuesLinearRegression) .setColor(ChartUtils.COLOR_VIOLET) .setHasPoints(false); @@ -464,22 +391,8 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { lines.add(linearRegressionLine); } - LineChartData lineData = new LineChartData(lines); - lineData.setAxisXBottom(new Axis(axisValues). - setHasLines(true). - setTextColor(Color.BLACK) - ); - - lineData.setAxisYLeft(new Axis(). - setHasLines(true). - setMaxLabelChars(5). - setTextColor(Color.BLACK) - ); - chartBottom.setLineChartData(lineData); - defaultTopViewport = new Viewport(0, chartBottom.getCurrentViewport().top+4, axisValues.size()-1, chartBottom.getCurrentViewport().bottom-4); - chartBottom.setMaximumViewport(defaultTopViewport); chartBottom.setCurrentViewport(defaultTopViewport); } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java index e65473ee..729a18b7 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java @@ -15,16 +15,132 @@ */ package com.health.openscale.gui.preferences; +import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.MultiSelectListPreference; +import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.text.method.DigitsKeyListener; import com.health.openscale.R; -public class GraphPreferences extends PreferenceFragment { +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class GraphPreferences extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + public static final String PREFERENCE_KEY_REGRESSION_LINE = "regressionLine"; + public static final String PREFERENCE_KEY_REGRESSION_LINE_ORDER = "regressionLineOrder"; + + private CheckBoxPreference regressionLine; + private EditTextPreference regressionLineOrder; + @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) + { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.graph_preferences); + + regressionLine = (CheckBoxPreference) findPreference(PREFERENCE_KEY_REGRESSION_LINE); + regressionLineOrder = (EditTextPreference) findPreference(PREFERENCE_KEY_REGRESSION_LINE_ORDER); + + regressionLineOrder.getEditText().setKeyListener(new DigitsKeyListener()); + + updateGraphPreferences(); + initSummary(getPreferenceScreen()); + } + + @Override + public void onResume() + { + super.onResume(); + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() + { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + { + updatePrefSummary(findPreference(key)); + updateGraphPreferences(); + } + + private void initSummary(Preference p) + { + if (p instanceof PreferenceGroup) + { + PreferenceGroup pGrp = (PreferenceGroup) p; + for (int i = 0; i < pGrp.getPreferenceCount(); i++) + { + initSummary(pGrp.getPreference(i)); + } + } + else + { + updatePrefSummary(p); + } + } + + public void updateGraphPreferences() + { + if (regressionLine.isChecked()) + { + regressionLineOrder.setEnabled(true); + } + else + { + regressionLineOrder.setEnabled(false); + } + } + + private void updatePrefSummary(Preference p) + { + if (p instanceof ListPreference) + { + ListPreference listPref = (ListPreference) p; + p.setSummary(listPref.getEntry()); + } + + if (p instanceof EditTextPreference) + { + EditTextPreference editTextPref = (EditTextPreference) p; + if (p.getTitle().toString().contains("assword")) + { + p.setSummary("******"); + } + else + { + p.setSummary(editTextPref.getText()); + } + } + + if (p instanceof MultiSelectListPreference) + { + MultiSelectListPreference editMultiListPref = (MultiSelectListPreference) p; + + CharSequence[] entries = editMultiListPref.getEntries(); + CharSequence[] entryValues = editMultiListPref.getEntryValues(); + List currentEntries = new ArrayList<>(); + Set currentEntryValues = editMultiListPref.getValues(); + + for (int i = 0; i < entries.length; i++) + { + if (currentEntryValues.contains(entryValues[i].toString())) currentEntries.add(entries[i].toString()); + } + + p.setSummary(currentEntries.toString()); + } } } diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 2f985fcf..47f00449 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -151,6 +151,7 @@ Initial weight Calculate average per day/month Regression weight line + Regression order Goal line diff --git a/android_app/app/src/main/res/xml/graph_preferences.xml b/android_app/app/src/main/res/xml/graph_preferences.xml index 66335eb8..1a55d731 100644 --- a/android_app/app/src/main/res/xml/graph_preferences.xml +++ b/android_app/app/src/main/res/xml/graph_preferences.xml @@ -5,4 +5,6 @@ - \ No newline at end of file + + + \ No newline at end of file