forked from upstream-mirrors/OpenTracks
Aggregated statistics by activity type. Fixes #112.
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class AggregatedStatisticsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
* Create a TrackStatistics object.
|
||||
*
|
||||
* @param totalDistance distance in meters.
|
||||
* @param totalTime total time in milliseconds.
|
||||
* @return TrackStatistics object.
|
||||
*/
|
||||
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setStartTime_ms(1000L); // Resulting start time
|
||||
statistics.setStopTime_ms(1000L + totalTime);
|
||||
statistics.setTotalTime(totalTime);
|
||||
statistics.setMovingTime(totalTime);
|
||||
statistics.setTotalDistance(totalDistance);
|
||||
statistics.setTotalElevationGain(50.0);
|
||||
statistics.setMaxSpeed(50.0); // Resulting max speed
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0); // Resulting min elevation
|
||||
|
||||
Track track = new Track();
|
||||
track.setIcon(TrackIconUtils.getIconValue(context, category));
|
||||
track.setCategory(category);
|
||||
track.setTrackStatistics(statistics);
|
||||
return track;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
Track track = createTrack(context, totalDistance, totalTime, biking);
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(track);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(1, aggregatedStatistics.getCount());
|
||||
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
|
||||
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_mountainBiking() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
String mountainBiking = context.getString(R.string.activity_type_mountain_biking);
|
||||
Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(track);
|
||||
|
||||
// then
|
||||
Assert.assertTrue(aggregatedStatistics.get(mountainBiking) != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_trailRunning() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
String trailRunning = context.getString(R.string.activity_type_trail_running);
|
||||
Track track = createTrack(context, totalDistance, totalTime, trailRunning);
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(track);
|
||||
|
||||
// then
|
||||
Assert.assertTrue(aggregatedStatistics.get(trailRunning) != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_twoBikingTracks() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
Track[] tracks = new Track[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
tracks[i] = createTrack(context, totalDistance, totalTime, biking);
|
||||
}
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
|
||||
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
|
||||
|
||||
// then
|
||||
Assert.assertEquals(1, aggregatedStatistics.getCount());
|
||||
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
|
||||
Assert.assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime * 2L, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_threeDifferentTracks() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
String running = context.getString(R.string.activity_type_running);
|
||||
String walking = context.getString(R.string.activity_type_walking);
|
||||
long totalTime = 2400000;
|
||||
Track[] tracks = new Track[]{
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, running),
|
||||
createTrack(context, totalDistance, totalTime, walking)
|
||||
};
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
|
||||
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
|
||||
aggregatedStatistics.aggregate(tracks[2]); // biking activity 3.
|
||||
|
||||
// then
|
||||
Assert.assertEquals(3, aggregatedStatistics.getCount());
|
||||
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
|
||||
Assert.assertTrue(aggregatedStatistics.get(running) != null);
|
||||
Assert.assertTrue(aggregatedStatistics.get(walking) != null);
|
||||
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
|
||||
Assert.assertEquals(1, aggregatedStatistics.get(running).getCountTracks());
|
||||
Assert.assertEquals(1, aggregatedStatistics.get(walking).getCountTracks());
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_severalTracksWithSeveralActivities() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
String running = context.getString(R.string.activity_type_running);
|
||||
String walking = context.getString(R.string.activity_type_walking);
|
||||
String driving = context.getString(R.string.activity_type_driving);
|
||||
Track[] tracks = new Track[]{
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, running),
|
||||
createTrack(context, totalDistance, totalTime, walking),
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, running),
|
||||
createTrack(context, totalDistance, totalTime, walking),
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, driving),
|
||||
};
|
||||
|
||||
|
||||
// when
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
for (Track track : tracks) {
|
||||
aggregatedStatistics.aggregate(track);
|
||||
|
||||
}
|
||||
|
||||
// then
|
||||
// 4 sports.
|
||||
Assert.assertEquals(4, aggregatedStatistics.getCount());
|
||||
|
||||
// There is a map for every sport.
|
||||
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
|
||||
Assert.assertTrue(aggregatedStatistics.get(running) != null);
|
||||
Assert.assertTrue(aggregatedStatistics.get(walking) != null);
|
||||
Assert.assertTrue(aggregatedStatistics.get(driving) != null);
|
||||
|
||||
// Number of tracks by sport.
|
||||
Assert.assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking.
|
||||
Assert.assertEquals(2, aggregatedStatistics.get(running).getCountTracks()); // Running.
|
||||
Assert.assertEquals(2, aggregatedStatistics.get(walking).getCountTracks()); // Walking.
|
||||
Assert.assertEquals(1, aggregatedStatistics.get(driving).getCountTracks()); // Driving.
|
||||
|
||||
// Biking.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime * 5, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Running.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime * 2, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Walking.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime * 2, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Driving.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
|
||||
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
Assert.assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,11 @@ limitations under the License.
|
||||
android:value=".SearchListActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AggregatedStatisticsActivity"
|
||||
android:screenOrientation="userPortrait"
|
||||
android:label="@string/menu_aggregated_statistics"/>
|
||||
|
||||
<activity
|
||||
android:name=".io.file.exporter.ExportActivity"
|
||||
android:theme="@style/ThemeCustomTransparent" />
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
|
||||
import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
|
||||
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
|
||||
|
||||
public class AggregatedStatisticsActivity extends AbstractActivity {
|
||||
|
||||
private ListView listView;
|
||||
private AggregatedStatisticsAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
listView = findViewById(R.id.aggregated_stats_list);
|
||||
|
||||
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
||||
viewModel.getAggregatedStats().observe(this, new Observer<AggregatedStatistics>() {
|
||||
@Override
|
||||
public void onChanged(AggregatedStatistics aggregatedStatistics) {
|
||||
if (aggregatedStatistics != null) {
|
||||
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics);
|
||||
listView.setAdapter(adapter);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.aggregated_stats;
|
||||
}
|
||||
}
|
||||
@@ -396,6 +396,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
this.invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
case R.id.track_list_aggregated_stats:
|
||||
intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_list_markers:
|
||||
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
|
||||
startActivity(intent);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
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.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
|
||||
|
||||
public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
|
||||
private AggregatedStatistics aggregatedStatistics;
|
||||
private Context context;
|
||||
|
||||
public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) {
|
||||
this.context = context;
|
||||
this.aggregatedStatistics = aggregatedStatistics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (aggregatedStatistics != null) {
|
||||
return aggregatedStatistics.getCount();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatedStatistics.AggregatedStatistic getItem(int position) {
|
||||
return aggregatedStatistics.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
// Not important because data are not database register but cooked data.
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
AggregatedStatistics.AggregatedStatistic aggregatedStats = getItem(position);
|
||||
ViewHolder viewHolder;
|
||||
|
||||
if (isSpeedSport(position)) {
|
||||
viewHolder = new ViewSpeedHolder();
|
||||
} else {
|
||||
viewHolder = new ViewPaceHolder();
|
||||
}
|
||||
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false);
|
||||
viewHolder.setValues(convertView, getIcon(position), getName(position), aggregatedStats);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private boolean isSpeedSport(int position) {
|
||||
String category = aggregatedStatistics.getSportName(position);
|
||||
return TrackIconUtils.isSpeedIcon(context, category);
|
||||
}
|
||||
|
||||
private int getIcon(int position) {
|
||||
String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistics.getSportName(position));
|
||||
return TrackIconUtils.getIconDrawable(iconValue);
|
||||
}
|
||||
|
||||
private String getName(int position) {
|
||||
return aggregatedStatistics.getSportName(position);
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
protected ImageView sportIcon;
|
||||
protected TextView typeLabel;
|
||||
protected TextView numTracks;
|
||||
protected TextView distance;
|
||||
protected TextView distanceUnit;
|
||||
protected TextView time;
|
||||
protected boolean metricsUnits;
|
||||
|
||||
public ViewHolder() {
|
||||
metricsUnits = PreferencesUtils.isMetricUnits(context);
|
||||
}
|
||||
|
||||
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
|
||||
sportIcon = view.findViewById(R.id.aggregated_stats_sport_icon);
|
||||
typeLabel = view.findViewById(R.id.aggregated_stats_type_label);
|
||||
numTracks = view.findViewById(R.id.aggregated_stats_num_tracks);
|
||||
distance = view.findViewById(R.id.aggregated_stats_distance);
|
||||
distanceUnit = view.findViewById(R.id.aggregated_stats_distance_unit);
|
||||
time = view.findViewById(R.id.aggregated_stats_time);
|
||||
|
||||
sportIcon.setImageResource(iconDrawable);
|
||||
typeLabel.setText(name);
|
||||
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStats.getCountTracks())));
|
||||
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStats.getTrackStatistics().getTotalDistance(), metricsUnits);
|
||||
distance.setText(parts.first);
|
||||
distanceUnit.setText(parts.second);
|
||||
|
||||
time.setText(StringUtils.formatElapsedTime(aggregatedStats.getTrackStatistics().getMovingTime()));
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewSpeedHolder extends ViewHolder {
|
||||
private TextView avgSpeed;
|
||||
private TextView avgSpeedUnit;
|
||||
private TextView avgSpeedLabel;
|
||||
private TextView maxSpeed;
|
||||
private TextView maxSpeedUnit;
|
||||
private TextView maxSpeedLabel;
|
||||
|
||||
@Override
|
||||
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
|
||||
super.setValues(view, iconDrawable, name, aggregatedStats);
|
||||
|
||||
avgSpeed = view.findViewById(R.id.aggregated_stats_avg_rate);
|
||||
avgSpeedUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
|
||||
avgSpeedLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
|
||||
maxSpeed = view.findViewById(R.id.aggregated_stats_max_rate);
|
||||
maxSpeedUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
|
||||
maxSpeedLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, true);
|
||||
avgSpeed.setText(parts.first);
|
||||
avgSpeedUnit.setText(parts.second);
|
||||
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, true);
|
||||
maxSpeed.setText(parts.first);
|
||||
maxSpeedUnit.setText(parts.second);
|
||||
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewPaceHolder extends ViewHolder {
|
||||
private TextView avgPace;
|
||||
private TextView avgPaceUnit;
|
||||
private TextView avgPaceLabel;
|
||||
private TextView maxPace;
|
||||
private TextView maxPaceUnit;
|
||||
private TextView maxPaceLabel;
|
||||
|
||||
@Override
|
||||
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
|
||||
super.setValues(view, iconDrawable, name, aggregatedStats);
|
||||
|
||||
avgPace = view.findViewById(R.id.aggregated_stats_avg_rate);
|
||||
avgPaceUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
|
||||
avgPaceLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
|
||||
|
||||
maxPace = view.findViewById(R.id.aggregated_stats_max_rate);
|
||||
maxPaceUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
|
||||
maxPaceLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, false);
|
||||
avgPace.setText(parts.first);
|
||||
avgPaceUnit.setText(parts.second);
|
||||
avgPaceLabel.setText(R.string.stats_average_moving_pace);
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, false);
|
||||
maxPace.setText(parts.first);
|
||||
maxPaceUnit.setText(parts.second);
|
||||
maxPaceLabel.setText(R.string.stats_fastest_pace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,4 +426,8 @@ public class StringUtils {
|
||||
}
|
||||
return new Pair<>(value, unit);
|
||||
}
|
||||
|
||||
public static String valueInParentheses(String text) {
|
||||
return "(" + text + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
public class AggregatedStatistics {
|
||||
|
||||
private HashMap<String, AggregatedStatistic> aggregatedStatistics = new HashMap<>();
|
||||
|
||||
public void aggregate(List<Track> tracks) {
|
||||
for (Track track : tracks) {
|
||||
aggregate(track);
|
||||
}
|
||||
}
|
||||
|
||||
public void aggregate(Track track) {
|
||||
String category = track.getCategory();
|
||||
if (aggregatedStatistics.containsKey(category)) {
|
||||
aggregatedStatistics.get(category).add(track.getTrackStatistics());
|
||||
} else {
|
||||
aggregatedStatistics.put(category, new AggregatedStatistic(track.getTrackStatistics()));
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return aggregatedStatistics.size();
|
||||
}
|
||||
|
||||
public String getSportName(int position) {
|
||||
return getKey(position);
|
||||
}
|
||||
|
||||
public AggregatedStatistic get(String category) {
|
||||
return aggregatedStatistics.get(category);
|
||||
}
|
||||
|
||||
private String getKey(int position) {
|
||||
return (new ArrayList<>(aggregatedStatistics.keySet())).get(position);
|
||||
}
|
||||
|
||||
public AggregatedStatistic getItem(int position) {
|
||||
return (new ArrayList<>(aggregatedStatistics.values())).get(position);
|
||||
}
|
||||
|
||||
public static class AggregatedStatistic {
|
||||
private TrackStatistics trackStatistics;
|
||||
private int countTracks;
|
||||
|
||||
public AggregatedStatistic(TrackStatistics trackStatistics) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.countTracks = 1;
|
||||
}
|
||||
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
return trackStatistics;
|
||||
}
|
||||
|
||||
public int getCountTracks() {
|
||||
return countTracks;
|
||||
}
|
||||
|
||||
void add(TrackStatistics statistics) {
|
||||
trackStatistics.merge(statistics);
|
||||
countTracks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
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.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
public class AggregatedStatisticsModel extends AndroidViewModel {
|
||||
|
||||
private MutableLiveData<AggregatedStatistics> aggregatedStats;
|
||||
|
||||
public AggregatedStatisticsModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public LiveData<AggregatedStatistics> getAggregatedStats() {
|
||||
if (aggregatedStats == null) {
|
||||
aggregatedStats = new MutableLiveData<>();
|
||||
loadAggregatedStats();
|
||||
}
|
||||
return aggregatedStats;
|
||||
}
|
||||
|
||||
private void loadAggregatedStats() {
|
||||
new Thread(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext());
|
||||
List<Track> tracks = contentProviderUtils.getAllTracks();
|
||||
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
|
||||
aggregatedStatistics.aggregate(tracks);
|
||||
|
||||
aggregatedStats.postValue(aggregatedStatistics);
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/aggregated_stats_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<!-- Guidelines -->
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="8dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="8dp" />
|
||||
|
||||
<!-- Icon and sport name -->
|
||||
<ImageView
|
||||
android:id="@+id/aggregated_stats_sport_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="@id/guideline1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_activity_bike_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_type_label"
|
||||
style="@style/StatsLargeLabel"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_sport_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="@string/activity_type_cycling"
|
||||
android:layout_marginLeft="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_num_tracks"
|
||||
style="@style/StatsLargeLabel"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_type_label"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_type_label"
|
||||
app:layout_constraintTop_toTopOf="@+id/aggregated_stats_type_label"
|
||||
tools:text="(1)"
|
||||
android:layout_marginLeft="4dp" />
|
||||
|
||||
<!-- Distance -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_distance"
|
||||
style="@style/StatsLargeValue"
|
||||
app:layout_constraintLeft_toLeftOf="@id/guideline1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="100000"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_distance_unit"
|
||||
style="@style/StatsUnit"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_distance"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_distance"
|
||||
tools:text="km" />
|
||||
|
||||
<!-- Time -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_time"
|
||||
style="@style/StatsLargeValue"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="00:00:00"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- Avg. rate -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_avg_rate"
|
||||
style="@style/StatsSmallValue"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintTop_toBottomOf="@id/aggregated_stats_distance"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline1"
|
||||
app:layout_constraintRight_toLeftOf="@+id/aggregated_stats_avg_rate_unit"
|
||||
android:layout_marginTop="24dp"
|
||||
tools:text="30" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_avg_rate_unit"
|
||||
style="@style/StatsUnit"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline2"
|
||||
tools:text="km/h" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_avg_rate_label"
|
||||
style="@style/StatsTinyLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline1"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline2"
|
||||
tools:text="@string/stats_average_moving_speed" />
|
||||
|
||||
<!-- Max. rate -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_max_rate"
|
||||
style="@style/StatsSmallValue"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintTop_toBottomOf="@id/aggregated_stats_time"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/aggregated_stats_max_rate_unit"
|
||||
android:layout_marginTop="24dp"
|
||||
tools:text="70" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_max_rate_unit"
|
||||
style="@style/StatsUnit"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintRight_toLeftOf="@id/guideline3"
|
||||
tools:text="km/h" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_max_rate_label"
|
||||
style="@style/StatsTinyLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@id/guideline3"
|
||||
tools:text="@string/stats_max_speed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -28,6 +28,11 @@ limitations under the License.
|
||||
android:title="@string/menu_start_gps"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_aggregated_stats"
|
||||
android:icon="@drawable/ic_statistics_24dp"
|
||||
android:title="@string/menu_aggregated_statistics"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_markers"
|
||||
android:icon="@drawable/ic_marker_show_24dp"
|
||||
|
||||
@@ -195,6 +195,13 @@ limitations under the License.
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:textColor">@android:color/white</item>
|
||||
</style>
|
||||
<!-- stats small label -->
|
||||
<style name="StatsTinyLabel" parent="@style/TextSmall">
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:layout_gravity">center</item>
|
||||
<item name="android:singleLine">false</item>
|
||||
<item name="android:textColor">@android:color/secondary_text_light_nodisable</item>
|
||||
</style>
|
||||
<!-- stats small value container -->
|
||||
<style name="StatsSmallValueContainer" parent="@style/StatsLargeValueContainer">
|
||||
<item name="android:layout_marginTop">-2dp</item>
|
||||
|
||||
Reference in New Issue
Block a user