Interval Stats: improve code and fixed some UI issues.

This commit is contained in:
Román
2020-08-24 17:24:22 +02:00
committed by Román Martínez
parent 287d246777
commit 65f42dd3e5
12 changed files with 118 additions and 147 deletions
@@ -23,9 +23,7 @@ public class IntervalStatisticsTest {
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
Pair<Track, TrackPoint[]> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints);
List<TrackPoint> trackPoints = Arrays.asList(pair.second);
return trackPoints;
return Arrays.asList(pair.second);
}
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
@@ -40,10 +38,10 @@ public class IntervalStatisticsTest {
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_1() {
public void testBuild_1() {
// With 50 points and interval distance of 1000m.
// given
// given
List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
@@ -127,8 +125,8 @@ public class IntervalStatisticsTest {
double totalDistance = 0d;
long totalTime = 0L;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance();
totalTime += ((i.getDistance() / i.getSpeed()) * UnitConversions.S_TO_MS);
totalDistance += i.getDistance_m();
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
}
// then
@@ -136,9 +134,9 @@ public class IntervalStatisticsTest {
Assert.assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
Assert.assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
for (int i = 0; i < intervalList.size() - 1; i++) {
Assert.assertEquals(intervalList.get(i).getDistance(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance();
Assert.assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m();
}
Assert.assertEquals(intervalList.get(intervalList.size() - 1).getDistance(), totalDistance, 0.01);
Assert.assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01);
}
}
@@ -381,9 +381,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatisticsRecordingFragment();
return StatisticsRecordingFragment.newInstance();
case 1:
return new IntervalsRecordingFragment();
return IntervalsRecordingFragment.newInstance();
case 2:
return ChartFragment.newInstance(false);
case 3:
@@ -21,7 +21,7 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
private boolean metricUnits;
private float accDistance = 0f;
private float sumDistance_m = 0f;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList) {
super(context, R.layout.interval_stats_list_item, intervalList);
@@ -48,11 +48,14 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
viewHolder = (ViewHolder) intervalView.getTag();
}
accDistance += interval.getDistance();
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), accDistance, metricUnits));
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, true);
sumDistance_m += interval.getDistance_m();
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
viewHolder.speed.setText(speedParts.first + " " + speedParts.second);
viewHolder.pace.setText(StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, false).first);
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
viewHolder.pace.setText(paceParts.first + " " + paceParts.second);
return intervalView;
}
@@ -20,7 +20,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
import de.dennisguse.opentracks.views.IntervalListView;
import de.dennisguse.opentracks.views.IntervalReverseListView;
/**
* A fragment to display the intervals from recording track.
@@ -29,29 +28,27 @@ public class IntervalsRecordingFragment extends Fragment implements IntervalList
private static final String TAG = IntervalsRecordingFragment.class.getSimpleName();
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
private IntervalStatisticsModel viewModel;
private IntervalReverseListView intervalListView;
private IntervalListView.IntervalReverseListView intervalListView;
private class IntervalRunner implements Runnable {
private boolean stopped = false;
public static Fragment newInstance() {
return new IntervalsRecordingFragment();
}
private final Runnable intervalRunner = new Runnable() {
@Override
public void run() {
if (!stopped) {
if (isResumed()) {
updateIntervals();
intervalHandler.postDelayed(intervalRunner, UI_UPDATE_INTERVAL);
}
}
};
public void stop() {
stopped = true;
}
}
private Handler intervalHandler = null;
private IntervalRunner intervalRunner = null;
private Handler intervalHandler;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -64,8 +61,9 @@ public class IntervalsRecordingFragment extends Fragment implements IntervalList
intervalHandler = new Handler();
intervalListView = new IntervalReverseListView(getActivity(), this);
intervalListView = new IntervalListView.IntervalReverseListView(getActivity(), this);
intervalListView.setId(View.generateViewId());
intervalListView.findViewById(R.id.interval_title_label).setVisibility(View.GONE);
LinearLayout linearLayout = view.findViewById(R.id.root_view);
linearLayout.removeAllViews();
linearLayout.addView(intervalListView);
@@ -76,19 +74,13 @@ public class IntervalsRecordingFragment extends Fragment implements IntervalList
@Override
public void onResume() {
super.onResume();
if (intervalRunner == null) {
intervalRunner = new IntervalRunner();
intervalRunner.run();
}
intervalHandler.post(intervalRunner);
}
@Override
public void onPause() {
super.onPause();
if (intervalRunner != null) {
intervalRunner.stop();
intervalRunner = null;
}
intervalHandler.removeCallbacks(intervalRunner);
}
@Override
@@ -106,7 +98,7 @@ public class IntervalsRecordingFragment extends Fragment implements IntervalList
* @param interval intervals will split in this interval if not null. If it's null then view model will use the default one.
*/
private void updateIntervals(@Nullable IntervalStatisticsModel.IntervalOption interval) {
if (viewModel == null | intervalListView == null) {
if (viewModel == null || intervalListView == null) {
return;
}
@@ -54,12 +54,12 @@ public class StatisticsRecordedFragment extends Fragment implements IntervalList
private static final String TRACK_ID_KEY = "trackId";
private TrackStatistics trackStatistics = null;
private TrackStatistics trackStatistics;
private String category = "";
private Track track;
private IntervalStatisticsModel viewModel = null;
private IntervalListView intervalListView = null;
private IntervalStatisticsModel viewModel;
private IntervalListView intervalListView;
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)) {
@@ -299,7 +299,6 @@ public class StatisticsRecordedFragment extends Fragment implements IntervalList
speedMovingUnit.setText(parts.second);
}
// Set intervals.
addIntervals();
}
@@ -55,13 +55,17 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint = null;
private TrackStatistics lastTrackStatistics = null;
private TrackPoint lastTrackPoint;
private TrackStatistics lastTrackStatistics;
private String category = "";
@Deprecated //TODO This should be handled somewhere else; not in the UI.
private int recordingGpsAccuracy;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@@ -51,8 +51,6 @@ 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
@@ -67,6 +65,10 @@ public class UnitConversions {
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
// multiplication factor to convert meters per second to kilometers per hour
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
// multiplication factor to convert miles to km
public static final double MI_TO_KM = 1 / KM_TO_MI;
// multiplication factor to convert miles to m
public static final double MI_TO_M = MI_TO_KM * KM_TO_M;
private UnitConversions() {
}
@@ -8,10 +8,10 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatistics {
List<Interval> intervalList;
List<Interval> intervalList = new ArrayList<>();
public void build(List<TrackPoint> trackPointList, float distanceInterval) {
intervalList = new ArrayList<>();
intervalList.clear();
if (trackPointList == null || trackPointList.size() == 0) {
return;
@@ -23,27 +23,22 @@ public class IntervalStatistics {
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();
interval.distance_m += prevTrackPoint.distanceTo(trackPoint);
interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime();
if (interval.distance >= distanceInterval) {
float adjustFactor = distanceInterval / interval.distance;
if (interval.distance_m >= distanceInterval) {
float adjustFactor = distanceInterval / interval.distance_m;
Interval adjustedInterval = new Interval(interval);
adjustedInterval.distance *= adjustFactor;
adjustedInterval.time *= adjustFactor;
adjustedInterval.adjust(adjustFactor);
intervalList.add(adjustedInterval);
Interval newInterval = new Interval();
newInterval.distance = interval.distance - adjustedInterval.distance;
newInterval.time = interval.time - adjustedInterval.time;
interval = newInterval;
interval = new Interval(interval.distance_m - adjustedInterval.distance_m, interval.time_ms - adjustedInterval.time_ms);
}
}
}
if (interval.distance > 1f) {
if (interval.distance_m > 1f) {
intervalList.add(interval);
}
}
@@ -53,28 +48,38 @@ public class IntervalStatistics {
}
public static class Interval {
private float distance = 0f;
private float time = 0f;
public float getDistance() {
return distance;
}
private float distance_m = 0f;
private float time_ms = 0f;
public Interval() {}
public Interval(float distance_m, float time_ms) {
this.distance_m = distance_m;
this.time_ms = time_ms;
}
public Interval(Interval i) {
distance = i.distance;
time = i.time;
distance_m = i.distance_m;
time_ms = i.time_ms;
}
public float getDistance_m() {
return distance_m;
}
public void adjust(float adjustFactor) {
distance_m *= adjustFactor;
time_ms *= adjustFactor;
}
/**
* @return speed of the interval in m/s.
*/
public float getSpeed() {
if (distance == 0f) {
public float getSpeed_ms() {
if (distance_m == 0f) {
return 0f;
}
return distance / (float) (time * UnitConversions.MS_TO_S);
return distance_m / (float) (time_ms * UnitConversions.MS_TO_S);
}
}
}
@@ -83,34 +83,8 @@ public class IntervalStatisticsModel extends AndroidViewModel {
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;
}
}
}
@@ -9,6 +9,7 @@ import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.R;
@@ -28,37 +29,35 @@ public class IntervalListView extends LinearLayout {
protected Spinner spinnerIntervals;
protected TextView spinnerIntervalsUnit;
protected Context context;
protected IntervalListListener listener;
protected 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 (spinnerIntervalsUnit != null) {
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(context) ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile));
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile));
}
}
};
public IntervalListView(Context context, IntervalListListener listener) {
super(context);
this.context = context;
this.listener = listener;
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
init();
}
protected 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()));
int[] intValues = Arrays.stream(IntervalStatisticsModel.IntervalOption.values()).mapToInt(i -> i.getValue()).toArray();
spinnerIntervals.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, Arrays.stream(intValues).mapToObj(String::valueOf).toArray(String[]::new)));
spinnerIntervalsUnit = findViewById(R.id.spinner_intervals_unit);
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(context) ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile));
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().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);
IntervalStatisticsModel.IntervalOption interval = IntervalStatisticsModel.IntervalOption.values()[i];
listener.intervalChanged(interval);
}
@@ -75,22 +74,47 @@ public class IntervalListView extends LinearLayout {
linearLayoutIntervals = null;
spinnerIntervals = null;
spinnerIntervalsUnit = null;
context = null;
listener = null;
}
public void display(List<IntervalStatistics.Interval> 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);
}
if (intervalList == null) {
return;
}
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);
}
/**
* LinearLayout view used to build a list of intervals in a reverse mode, the last one will appear in the first position on the LinearLayout.
* This class is an specialization of {@link IntervalListView} that display the views contained in the LinearLayout in a reverse mode.
*/
public static class IntervalReverseListView extends IntervalListView {
public IntervalReverseListView(Context context, IntervalListListener listener) {
super(context, listener);
}
public void display(List<IntervalStatistics.Interval> intervalList) {
if (intervalList == null) {
return;
}
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, 0);
}
}
}
}
@@ -1,31 +0,0 @@
package de.dennisguse.opentracks.views;
import android.content.Context;
import android.view.View;
import java.util.List;
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
/**
* LinearLayout view used to build a list of intervals in a reverse mode, the last one will appear in the first position on the LinearLayout.
* This class is an specialization of {@link IntervalListView} that display the views contained in the LinearLayout in a reverse mode.
*/
public class IntervalReverseListView extends IntervalListView {
public IntervalReverseListView(Context context, IntervalListListener listener) {
super(context, listener);
}
public void display(List<IntervalStatistics.Interval> 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, 0);
}
}
}
}
+2 -1
View File
@@ -6,6 +6,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/interval_title_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/StatsSmallLabel"
@@ -27,7 +28,7 @@
android:id="@+id/spinner_intervals"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/SpinnerCustom"/>
android:gravity="center" />
<TextView
android:id="@+id/spinner_intervals_unit"