diff --git a/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java new file mode 100644 index 000000000..88983cf5a --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java @@ -0,0 +1,63 @@ +package de.dennisguse.opentracks.adapters; + +import android.content.Context; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.util.StringUtils; +import de.dennisguse.opentracks.viewmodels.IntervalStatistics; + +public class IntervalStatisticsAdapter extends ArrayAdapter { + + private boolean metricUnits; + + public IntervalStatisticsAdapter(Context context, List intervalList) { + super(context, R.layout.interval_stats_list_item, intervalList); + metricUnits = PreferencesUtils.isMetricUnits(context); + } + + @NonNull + @Override + public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) { + IntervalStatistics.Interval interval = getItem(position); + ViewHolder viewHolder; + + if (intervalView == null) { + viewHolder = new ViewHolder(); + + intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false); + + viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance); + viewHolder.speed = intervalView.findViewById(R.id.interval_item_speed); + viewHolder.pace = intervalView.findViewById(R.id.interval_item_pace); + + intervalView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) intervalView.getTag(); + } + + viewHolder.distance.setText(StringUtils.formatDistance(getContext(), interval.getDistance(), metricUnits)); + Pair speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, true); + viewHolder.speed.setText(speedParts.first + " " + speedParts.second); + viewHolder.pace.setText(StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, false).first); + + return intervalView; + } + + private static class ViewHolder { + private TextView distance; + private TextView speed; + private TextView pace; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java index bfa9412f3..395e4d0ce 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java @@ -24,12 +24,14 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackRecordedActivity; @@ -39,6 +41,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.TrackIconUtils; +import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; +import de.dennisguse.opentracks.views.IntervalListView; /** * A fragment to display track statistics to the user. @@ -46,7 +50,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils; * @author Sandor Dornbush * @author Rodrigo Damazio */ -public class StatisticsRecordedFragment extends Fragment { +public class StatisticsRecordedFragment extends Fragment implements IntervalListView.IntervalListListener { private static final String TRACK_ID_KEY = "trackId"; @@ -54,6 +58,9 @@ public class StatisticsRecordedFragment extends Fragment { private String category = ""; private Track track; + private IntervalStatisticsModel viewModel = null; + private IntervalListView intervalListView = null; + private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (isResumed()) { @@ -72,6 +79,7 @@ public class StatisticsRecordedFragment extends Fragment { } /* Views */ + private ViewGroup rootView; private TextView totalTimeValueView; private TextView distanceValue; private TextView distanceUnit; @@ -110,6 +118,8 @@ public class StatisticsRecordedFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + rootView = view.findViewById(R.id.root_view); + totalTimeValueView = view.findViewById(R.id.stats_total_time_value); distanceValue = view.findViewById(R.id.stats_distance_value); @@ -153,6 +163,21 @@ public class StatisticsRecordedFragment extends Fragment { }); } + private void addIntervals() { + intervalListView = new IntervalListView(getActivity(), this); + intervalListView.setId(View.generateViewId()); + LinearLayout linearLayoutExtra = rootView.findViewById(R.id.linear_layout_extra); + linearLayoutExtra.removeAllViews(); + linearLayoutExtra.addView(intervalListView); + + viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class); + viewModel.getIntervalStats(track.getId()).observe(getActivity(), intervalStatistics -> { + if (intervalStatistics != null) { + intervalListView.display(intervalStatistics.getIntervalList()); + } + }); + } + @Override public void onResume() { super.onResume(); @@ -269,5 +294,20 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingValue.setText(parts.first); speedMovingUnit.setText(parts.second); } + + // Set intervals. + addIntervals(); + } + + @Override + public void intervalChanged(IntervalStatisticsModel.IntervalOption interval) { + if (viewModel != null && intervalListView != null) { + viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class); + viewModel.getIntervalStats(track.getId(), interval).observe(getActivity(), intervalStatistics -> { + if (intervalStatistics != null) { + intervalListView.display(intervalStatistics.getIntervalList()); + } + }); + } } } diff --git a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java index 7f878afd8..aefdf51ea 100644 --- a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java +++ b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java @@ -51,6 +51,8 @@ public class UnitConversions { public static final double MM_TO_M = 0.001; // Distance + // multiplication factor to convert miles to meters + public static final double MI_TO_M = 1609.344; // multiplication factor to convert miles to feet public static final double MI_TO_FT = 5280.0; // multiplication factor to convert feet to miles diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java new file mode 100644 index 000000000..34e3c34c2 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java @@ -0,0 +1,67 @@ +package de.dennisguse.opentracks.viewmodels; + +import java.util.ArrayList; +import java.util.List; + +import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.util.LocationUtils; +import de.dennisguse.opentracks.util.UnitConversions; + +public class IntervalStatistics { + List intervalList; + + public IntervalStatistics() { + intervalList = new ArrayList<>(); + } + + public void build(List trackPointList, float distanceInterval) { + intervalList = new ArrayList<>(); + + if (trackPointList == null || trackPointList.size() == 0) { + return; + } + + Interval interval = new Interval(); + for (int i = 1; i < trackPointList.size(); i++) { + TrackPoint prevTrackPoint = trackPointList.get(i - 1); + TrackPoint trackPoint = trackPointList.get(i); + + if (LocationUtils.isValidLocation(trackPoint.getLocation()) && LocationUtils.isValidLocation(prevTrackPoint.getLocation())) { + interval.distance += prevTrackPoint.distanceTo(trackPoint); + interval.time += trackPoint.getTime() - prevTrackPoint.getTime(); + + if (interval.distance >= distanceInterval) { + intervalList.add(interval); + interval = new Interval(); + } + } + } + + if (interval.distance > 1f) { + intervalList.add(interval); + } + } + + public List getIntervalList() { + return intervalList; + } + + public static class Interval { + float distance = 0f; + float time = 0f; + + public float getDistance() { + return distance; + } + + /** + * @return speed of the interval in m/s. + */ + public float getSpeed() { + if (distance == 0f) { + return 0f; + } + return distance / (float) (time * UnitConversions.MS_TO_S); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java new file mode 100644 index 000000000..89c5626e7 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java @@ -0,0 +1,98 @@ +package de.dennisguse.opentracks.viewmodels; + +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.util.List; + +import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.content.provider.ContentProviderUtils; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.util.UnitConversions; + +public class IntervalStatisticsModel extends AndroidViewModel { + + private MutableLiveData intervalStats; + private IntervalOption interval; + + public IntervalStatisticsModel(@NonNull Application application) { + super(application); + } + + public LiveData getIntervalStats(long trackId, IntervalOption interval) { + if (intervalStats == null || this.interval != interval) { + intervalStats = new MutableLiveData<>(); + this.interval = interval; + loadIntervalStats(trackId); + } + return intervalStats; + } + + public LiveData getIntervalStats(long trackId) { + return getIntervalStats(trackId, IntervalOption.OPTION_1); + } + + private void loadIntervalStats(long trackId) { + new Thread(() -> { + Context context = getApplication().getApplicationContext(); + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + List trackPointList = contentProviderUtils.getTrackPoints(trackId); + + IntervalStatistics intervalStatistics = new IntervalStatistics(); + float distanceInterval = PreferencesUtils.isMetricUnits(context) ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M); + intervalStatistics.build(trackPointList, distanceInterval); + + intervalStats.postValue(intervalStatistics); + }).start(); + } + + public enum IntervalOption { + OPTION_1(1), + OPTION_2(2), + OPTION_3(3), + OPTION_4(4), + OPTION_5(5), + OPTION_10(10); + + private int value; + + IntervalOption(int value) { + this.value = value; + } + + /** + * @param pos position of the interval option. + * @return the interval option that is in the position pos. + */ + public static IntervalOption getIntervalOption(int pos) { + if (values().length > pos) { + return values()[pos]; + } else { + return OPTION_1; + } + } + + public int getValue() { + return value; + } + + /** + * @return a string array with all options. + */ + public static String[] getAllValues() { + IntervalOption[] options = values(); + String[] values = new String[options.length]; + + for (int i = 0; i < options.length; i++) { + values[i] = String.valueOf(options[i].getValue()); + } + + return values; + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java b/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java new file mode 100644 index 000000000..4d0aed501 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java @@ -0,0 +1,72 @@ +package de.dennisguse.opentracks.views; + +import android.content.Context; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.viewmodels.IntervalStatistics; +import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; + +public class IntervalListView extends LinearLayout { + + private IntervalStatisticsAdapter adapter; + private LinearLayout linearLayoutIntervals; + private Spinner spinnerIntervals; + private TextView spinnerIntervalsUnit; + + private Context context; + private IntervalListListener listener; + + public IntervalListView(Context context, IntervalListListener listener) { + super(context); + this.context = context; + this.listener = listener; + init(); + } + + private void init() { + inflate(getContext(), R.layout.interval_list_view, this); + linearLayoutIntervals = findViewById(R.id.interval_list); + spinnerIntervals = findViewById(R.id.spinner_intervals); + spinnerIntervals.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.getAllValues())); + spinnerIntervalsUnit = findViewById(R.id.spinner_intervals_unit); + spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(context) ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile)); + + spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + IntervalStatisticsModel.IntervalOption interval = IntervalStatisticsModel.IntervalOption.getIntervalOption(i); + listener.intervalChanged(interval); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + } + + public void display(List intervalList) { + if (intervalList != null) { + adapter = new IntervalStatisticsAdapter(getContext(), intervalList); + linearLayoutIntervals.removeAllViews(); + for (int i = 0; i < adapter.getCount(); i++) { + View intervalView = adapter.getView(i, null, linearLayoutIntervals); + linearLayoutIntervals.addView(intervalView); + } + } + } + + public interface IntervalListListener { + void intervalChanged(IntervalStatisticsModel.IntervalOption interval); + } +} diff --git a/src/main/res/layout/interval_list_view.xml b/src/main/res/layout/interval_list_view.xml new file mode 100644 index 000000000..28a0f16af --- /dev/null +++ b/src/main/res/layout/interval_list_view.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/interval_stats_list_item.xml b/src/main/res/layout/interval_stats_list_item.xml new file mode 100644 index 000000000..75801c099 --- /dev/null +++ b/src/main/res/layout/interval_stats_list_item.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/statistics_recorded.xml b/src/main/res/layout/statistics_recorded.xml index 92e53a07c..5ae39ca1a 100644 --- a/src/main/res/layout/statistics_recorded.xml +++ b/src/main/res/layout/statistics_recorded.xml @@ -219,5 +219,15 @@ app:layout_constraintRight_toLeftOf="@+id/guideline3" app:layout_constraintTop_toBottomOf="@+id/stats_max_speed_value" /> + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0b4a2af54..ccca985f0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -417,6 +417,8 @@ limitations under the License. Speed Distance Total time + Intervals + Split interval Track %1$d Recording your track…