Aggregate statistics are now computed in the database.

This commit is contained in:
Dennis Guse
2025-11-05 20:10:42 +01:00
parent ca64f2c749
commit 221b61c7f1
12 changed files with 197 additions and 304 deletions
@@ -1,81 +0,0 @@
package de.dennisguse.opentracks.ui.aggregatedStatistics;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
public class AggregatedStatistics {
private final List<AggregatedStatistic> dataList;
public AggregatedStatistics(@NonNull List<Track> tracks) {
Map<String, List<Track>> groupedByActivityType = tracks.stream().collect(Collectors.groupingBy(Track::getActivityTypeLocalized));
List<Pair<String, List<Track>>> groupedByActivityTypeList = new ArrayList<>();
groupedByActivityType.forEach((activityTypeLocalized, trackList) -> groupedByActivityTypeList.add(new Pair<>(activityTypeLocalized, trackList)));
dataList = groupedByActivityTypeList.parallelStream()
.map(i -> new AggregatedStatistic(i.first, i.second))
.sorted((o1, o2) -> {
if (o1.getCountTracks() == o2.getCountTracks()) {
return o1.getActivityTypeLocalized().compareTo(o2.getActivityTypeLocalized());
}
return (o1.getCountTracks() < o2.getCountTracks() ? 1 : -1);
}).toList();
}
public int getCount() {
return dataList.size();
}
@VisibleForTesting
public AggregatedStatistic get(String activityTypeLocalized) {
return dataList.stream().filter(i -> i.getActivityTypeLocalized().equals(activityTypeLocalized)).findFirst().get();
}
public AggregatedStatistic getItem(int position) {
return dataList.get(position);
}
public static class AggregatedStatistic {
final String activityTypeLocalized;
final int countTracks;
final Duration totalMovingTime;
final Distance totalDistance;
final Speed avgMovingSpeed;
final Speed maxSpeed;
public AggregatedStatistic(String activityTypeLocalized, List<Track> tracks) {
this.activityTypeLocalized = activityTypeLocalized;
this.countTracks = tracks.size();
totalMovingTime = tracks.stream().map(t -> t.getStatistics().movingTime()).reduce(Duration.ZERO, Duration::plus);
totalDistance = tracks.stream().map(t -> t.getStatistics().totalDistance()).reduce(Distance.ZERO, Distance::plus);
maxSpeed = tracks.stream().map(t -> t.getStatistics().maxSpeed()).max((o1, o2) -> (int) (o1.speed_mps() - o2.speed_mps())).orElse(Speed.ZERO);
avgMovingSpeed = Speed.of(totalDistance, totalMovingTime);
}
public String getActivityTypeLocalized() {
return activityTypeLocalized;
}
public int getCountTracks() {
return countTracks;
}
}
}
@@ -55,12 +55,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi
viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats(selection).observe(this, aggregatedStatistics -> {
if ((aggregatedStatistics == null || aggregatedStatistics.getCount() == 0) && !selection.isEmpty()) {
viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results));
}
if (aggregatedStatistics != null) {
adapter.swapData(aggregatedStatistics);
}
adapter.swapData(aggregatedStatistics);
checkListEmpty();
});
@@ -71,6 +66,9 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi
if (adapter.getItemCount() == 0) {
viewBinding.aggregatedStatsList.setVisibility(View.GONE);
viewBinding.aggregatedStatsEmptyView.setVisibility(View.VISIBLE);
if (!selection.isEmpty()) {
viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results));
}
} else {
viewBinding.aggregatedStatsList.setVisibility(View.VISIBLE);
viewBinding.aggregatedStatsEmptyView.setVisibility(View.GONE);
@@ -8,10 +8,10 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.AggregatedStatistic;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
@@ -22,10 +22,10 @@ import de.dennisguse.opentracks.util.StringUtils;
public class AggregatedStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private AggregatedStatistics aggregatedStatistics;
private List<AggregatedStatistic> aggregatedStatistics;
private final Context context;
public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) {
public AggregatedStatisticsAdapter(Context context, @NonNull List<AggregatedStatistic> aggregatedStatistics) {
this.context = context;
this.aggregatedStatistics = aggregatedStatistics;
}
@@ -40,9 +40,9 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter<RecyclerVi
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ViewHolder viewHolder = (ViewHolder) holder;
AggregatedStatistics.AggregatedStatistic aggregatedStatistic = aggregatedStatistics.getItem(position);
AggregatedStatistic aggregatedStatistic = aggregatedStatistics.get(position);
String type = aggregatedStatistic.getActivityTypeLocalized();
String type = aggregatedStatistic.activityTypeLocalized();
if (ActivityType.findByLocalizedString(context, type).isShowSpeedPreferred()) {
viewHolder.setSpeed(aggregatedStatistic);
} else {
@@ -52,23 +52,18 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter<RecyclerVi
@Override
public int getItemCount() {
if (aggregatedStatistics == null) {
return 0;
}
return aggregatedStatistics.getCount();
return aggregatedStatistics.size();
}
public void swapData(AggregatedStatistics aggregatedStatistics) {
public void swapData(List<AggregatedStatistic> aggregatedStatistics) {
this.aggregatedStatistics = aggregatedStatistics;
this.notifyDataSetChanged();
}
public List<String> getCategories() {
List<String> categories = new ArrayList<>();
for (int i = 0; i < aggregatedStatistics.getCount(); i++) {
categories.add(aggregatedStatistics.getItem(i).getActivityTypeLocalized());
}
return categories;
return aggregatedStatistics.stream()
.map(AggregatedStatistic::activityTypeLocalized)
.toList();
}
private class ViewHolder extends RecyclerView.ViewHolder {
@@ -81,38 +76,38 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter<RecyclerVi
this.viewBinding = viewBinding;
}
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
public void setSpeed(AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
{
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.avgMovingSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getAverageMovingSpeed());
viewBinding.aggregatedStatsAvgRate.setText(parts.first);
viewBinding.aggregatedStatsAvgRateUnit.setText(parts.second);
viewBinding.aggregatedStatsAvgRateLabel.setText(context.getString(R.string.stats_average_moving_speed));
}
{
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed());
viewBinding.aggregatedStatsMaxRate.setText(parts.first);
viewBinding.aggregatedStatsMaxRateUnit.setText(parts.second);
viewBinding.aggregatedStatsMaxRateLabel.setText(context.getString(R.string.stats_max_speed));
}
}
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
public void setPace(AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
{
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.avgMovingSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getAverageMovingSpeed());
viewBinding.aggregatedStatsAvgRate.setText(parts.first);
viewBinding.aggregatedStatsAvgRateUnit.setText(parts.second);
viewBinding.aggregatedStatsAvgRateLabel.setText(context.getString(R.string.stats_average_moving_pace));
}
{
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed());
viewBinding.aggregatedStatsMaxRate.setText(parts.first);
viewBinding.aggregatedStatsMaxRateUnit.setText(parts.second);
viewBinding.aggregatedStatsMaxRateLabel.setText(R.string.stats_fastest_pace);
@@ -120,27 +115,27 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter<RecyclerVi
}
//TODO Check preference handling.
private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String activityType = aggregatedStatistic.getActivityTypeLocalized();
private void setCommonValues(AggregatedStatistic aggregatedStatistic) {
String activityType = aggregatedStatistic.activityTypeLocalized();
reportSpeed = PreferencesUtils.isReportSpeed(activityType);
unitSystem = PreferencesUtils.getUnitSystem();
viewBinding.activityIcon.setImageResource(getIcon(aggregatedStatistic));
viewBinding.aggregatedStatsTypeLabel.setText(activityType);
viewBinding.aggregatedStatsNumTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
viewBinding.aggregatedStatsNumTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.countTracks())));
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context).getDistanceParts(aggregatedStatistic.totalDistance);
.build(context).getDistanceParts(aggregatedStatistic.totalDistance());
viewBinding.aggregatedStatsDistance.setText(parts.first);
viewBinding.aggregatedStatsDistanceUnit.setText(parts.second);
viewBinding.aggregatedStatsTime.setText(StringUtils.formatElapsedTime(aggregatedStatistic.totalMovingTime));
viewBinding.aggregatedStatsTime.setText(StringUtils.formatElapsedTime(aggregatedStatistic.totalMovingTime()));
}
private int getIcon(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String localizedActivityType = aggregatedStatistic.getActivityTypeLocalized();
private int getIcon(AggregatedStatistic aggregatedStatistic) {
String localizedActivityType = aggregatedStatistic.activityTypeLocalized();
return ActivityType.findByLocalizedString(context, localizedActivityType)
.getIconDrawableId();
}
@@ -10,19 +10,19 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List;
import de.dennisguse.opentracks.data.AggregatedStatistic;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackSelection;
import de.dennisguse.opentracks.data.models.Track;
public class AggregatedStatisticsModel extends AndroidViewModel {
private MutableLiveData<AggregatedStatistics> aggregatedStats;
private MutableLiveData<List<AggregatedStatistic>> aggregatedStats;
public AggregatedStatisticsModel(@NonNull Application application) {
super(application);
}
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable TrackSelection selection) {
public LiveData<List<AggregatedStatistic>> getAggregatedStats(@Nullable TrackSelection selection) {
if (aggregatedStats == null) {
aggregatedStats = new MutableLiveData<>();
loadAggregatedStats(selection);
@@ -41,11 +41,7 @@ public class AggregatedStatisticsModel extends AndroidViewModel {
private void loadAggregatedStats(TrackSelection selection) {
new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext());
List<Track> tracks = selection != null ? contentProviderUtils.getTracks(selection) : contentProviderUtils.getTracks();
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
aggregatedStats.postValue(aggregatedStatistics);
aggregatedStats.postValue(contentProviderUtils.getAggregatedStatisticsForTracks(selection));
}).start();
}
}