Convert metricUnits boolean into an enum.

Fixes #1199.
This commit is contained in:
Dennis Guse
2022-04-26 22:00:53 +02:00
parent db5c9479df
commit a5b44175a3
34 changed files with 464 additions and 318 deletions
@@ -5,12 +5,13 @@ import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class PreferenceMetricUnitRule implements TestRule {
private final boolean metricUnits;
public class PreferenceUnitSystemRule implements TestRule {
private final UnitSystem unit;
public PreferenceMetricUnitRule(boolean metricUnits) {
this.metricUnits = metricUnits;
public PreferenceUnitSystemRule(UnitSystem unit) {
this.unit = unit;
}
@Override
@@ -18,13 +19,13 @@ public class PreferenceMetricUnitRule implements TestRule {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits();
final UnitSystem previousUnitSystem = PreferencesUtils.getUnitSystem();
try {
PreferencesUtils.setMetricUnits(metricUnits);
PreferencesUtils.setUnit(unit);
base.evaluate();
} finally {
PreferencesUtils.setMetricUnits(previousMetricUnits);
PreferencesUtils.setUnit(previousUnitSystem);
}
}
};
@@ -14,6 +14,7 @@ import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class)
@@ -26,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, false);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -39,7 +40,7 @@ public class ChartPointTest {
statistics.setTotalDistance(Distance.of(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
// then
assertEquals(1, (long) point.getTimeOrDistance());
@@ -51,7 +52,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertEquals(50, point.getAltitude(), 0.01);
@@ -65,7 +66,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertNull(point.getHeartRate());
@@ -84,7 +85,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
@@ -10,6 +10,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(AndroidJUnit4.class)
public class DistanceFormatterTest {
@@ -19,7 +21,7 @@ public class DistanceFormatterTest {
public void testFormatDistance_metric() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(true)
.setUnit(UnitSystem.METRIC)
.build(context);
// A large number in metric
@@ -32,7 +34,7 @@ public class DistanceFormatterTest {
public void testFormatDistance_imperial() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(false)
.setUnit(UnitSystem.IMPERIAL)
.build(context);
// A large number in imperial
@@ -10,6 +10,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(AndroidJUnit4.class)
public class SpeedFormatterTest {
@@ -19,7 +21,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_pace_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(true)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
@@ -33,7 +35,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_pace_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(false)
.setUnit(UnitSystem.IMPERIAL)
.setReportSpeedOrPace(false)
.build(context);
@@ -47,7 +49,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_speed_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(true)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(true)
.build(context);
@@ -59,7 +61,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_speed_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(false)
.setUnit(UnitSystem.IMPERIAL)
.setReportSpeedOrPace(true)
.build(context);
@@ -71,7 +73,7 @@ public class SpeedFormatterTest {
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setMetricUnits(true)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
@@ -19,6 +19,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(MockitoJUnitRunner.class)
@@ -48,7 +49,7 @@ public class TrackRecordingServiceNotificationManagerTest {
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setMetricUnits(true);
subject.setUnitSystem(UnitSystem.METRIC);
// when
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -54,7 +55,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
@@ -76,7 +77,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
@@ -92,7 +93,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
@@ -114,7 +115,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
@@ -130,7 +131,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
@@ -152,7 +153,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
@@ -168,7 +169,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
@@ -190,7 +191,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
@@ -215,7 +216,7 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
@@ -23,6 +23,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
@@ -61,7 +62,7 @@ public class StatisticDataBuilderTest extends TestCase {
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 3);
@@ -100,7 +101,7 @@ public class StatisticDataBuilderTest extends TestCase {
layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 18);
@@ -4,8 +4,6 @@ import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -28,9 +26,7 @@ public class Startup extends Application {
PreferencesUtils.initPreferences(this, getResources());
// Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString( R.string.stats_units_key, "").equals("")) {
PreferencesUtils.setMetricUnits(!Locale.US.equals(Locale.getDefault()));
}
PreferencesUtils.applyDefaultUnit();
PreferencesUtils.applyNightMode();
@@ -59,6 +59,7 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
@@ -88,7 +89,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
// Preferences
private boolean metricUnits = true;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
@@ -112,7 +113,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (key != null) {
runOnUiThread(() -> {
@@ -192,7 +193,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = DistanceFormatter.Builder()
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(TrackListActivity.this).formatDistance(Distance.of(cursor.getDouble(totalDistanceIndex)));
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@@ -77,9 +78,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits();
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
viewBinding.chartView.setMetricUnits(metricUnits);
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
if (unitSystem != viewBinding.chartView.getUnitSystem()) {
viewBinding.chartView.setUnitSystem(unitSystem);
runOnUiThread(() -> {
if (isResumed()) {
viewBinding.chartView.requestLayout();
@@ -189,7 +190,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getMetricUnits());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -8,6 +8,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
public class ChartPoint {
@@ -28,20 +29,20 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, boolean metricUnits) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits);
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
}
if (smoothedSpeed != null) {
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
speed = smoothedSpeed.to(unitSystem);
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
}
if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate().getBPM();
@@ -27,6 +27,7 @@ import androidx.annotation.NonNull;
import java.text.NumberFormat;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
/**
@@ -217,11 +218,15 @@ abstract class ChartValueSeries {
return maxMarkerValue;
}
/**
* Gets the title id.
*/
int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
int getTitleId(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return metricTitleId;
case IMPERIAL:
return imperialTitleId;
default:
throw new RuntimeException("Not implemented");
}
}
/**
@@ -46,6 +46,7 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -116,7 +117,7 @@ public class ChartView extends View {
private int effectiveHeight = 0;
private boolean chartByDistance = false;
private boolean metricUnits = true;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed = true;
private boolean showPointer = false;
@@ -371,17 +372,12 @@ public class ChartView extends View {
this.chartByDistance = chartByDistance;
}
public boolean getMetricUnits() {
return metricUnits;
public UnitSystem getUnitSystem() {
return unitSystem;
}
/**
* Sets metric units.
*
* @param value true to use metric units
*/
public void setMetricUnits(boolean value) {
metricUnits = value;
public void setUnitSystem(UnitSystem value) {
unitSystem = value;
}
public boolean getReportSpeed() {
@@ -677,7 +673,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Paint paint = chartValueSeries.getTitlePaint();
int x = (int) (0.5 * width) + getScrollX();
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
@@ -696,7 +692,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
if (rect.height() > lineHeight) {
lineHeight = rect.height();
@@ -731,7 +727,14 @@ public class ChartView extends View {
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
switch (unitSystem) {
case METRIC:
return context.getString(R.string.unit_kilometer);
case IMPERIAL:
return context.getString(R.string.unit_mile);
default:
throw new RuntimeException("Not implemented");
}
} else {
return context.getString(R.string.description_time);
}
@@ -986,7 +989,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
return marker.getLength().toKM_Miles(metricUnits);
return marker.getLength().toKM_Miles(unitSystem);
} else {
return marker.getDuration().toMillis();
}
@@ -5,6 +5,8 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Distance {
public static Distance of(double distance_m) {
@@ -43,11 +45,14 @@ public class Distance {
return of(0.1 * distance_dm);
}
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.ofKilometer(1);
} else {
return Distance.ofMile(1);
public static Distance one(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return Distance.ofKilometer(1);
case IMPERIAL:
return Distance.ofMile(1);
default:
throw new RuntimeException("Not implemented");
}
}
@@ -109,12 +114,26 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI;
}
public double toKM_Miles(boolean metricUnit) {
return metricUnit ? toKM() : toMI();
public double toKM_Miles(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKM();
case IMPERIAL:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public double toM_FT(boolean metricUnit) {
return metricUnit ? toM() : toFT();
public double toM_FT(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toM();
case IMPERIAL:
return toFT();
default:
throw new RuntimeException("Not implemented");
}
}
@Override
@@ -4,7 +4,10 @@ import android.content.Context;
import android.content.res.Resources;
import android.util.Pair;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class DistanceFormatter {
@@ -13,12 +16,13 @@ public class DistanceFormatter {
private final int decimalCount;
private final boolean metricUnits;
private final UnitSystem unitSystem;
private DistanceFormatter(Resources resources, int decimalCount, boolean metricUnits) {
private DistanceFormatter(Resources resources, int decimalCount, UnitSystem unitSystem) {
this.resources = resources;
this.decimalCount = decimalCount;
this.metricUnits = metricUnits;
this.unitSystem = unitSystem;
assert unitSystem != null;
}
public String formatDistance(Distance distance) {
@@ -34,26 +38,35 @@ public class DistanceFormatter {
/**
* Get the formatted distance with unit.
*
* @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) {
return new Pair<>(null, resources.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
switch (unitSystem) {
case METRIC:
return new Pair<>(null, resources.getString(R.string.unit_meter));
case IMPERIAL:
return new Pair<>(null, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
}
if (metricUnits) {
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
} else {
if (distance.greaterThan(Distance.ofMile(0.5))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
switch (unitSystem) {
case METRIC:
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
case IMPERIAL:
if (distance.greaterThan(Distance.ofMile(0.5))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
default:
throw new RuntimeException("Not implemented");
}
}
@@ -65,11 +78,10 @@ public class DistanceFormatter {
private int decimalCount;
private boolean metricUnits;
private UnitSystem unitSystem;
public Builder() {
decimalCount = 2;
metricUnits = true;
}
public Builder setDecimalCount(int decimalCount) {
@@ -77,13 +89,13 @@ public class DistanceFormatter {
return this;
}
public Builder setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
public Builder setUnit(@Nullable UnitSystem unitSystem) {
this.unitSystem = unitSystem;
return this;
}
public DistanceFormatter build(Resources resource) {
return new DistanceFormatter(resource, decimalCount, metricUnits);
return new DistanceFormatter(resource, decimalCount, unitSystem);
}
public DistanceFormatter build(Context context) {
@@ -6,6 +6,7 @@ import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Speed {
@@ -92,35 +93,37 @@ public class Speed {
return toKMH() * UnitConversions.KM_TO_MI;
}
public Duration toPace(boolean metricUnit) {
public Duration toPace(UnitSystem unitSystem) {
if (isZero()) {
return Duration.ofSeconds(0);
}
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
double distance = speed_mps;
switch (unitSystem) { //TODO Can we use to(UnitSystem)?
case METRIC:
distance *= UnitConversions.M_TO_KM;
break;
case IMPERIAL:
distance *= UnitConversions.M_TO_MI;
break;
default:
throw new RuntimeException("Not implemented");
}
return Duration.ofSeconds(Math.round(1 / distance));
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KMH : Unit.MPH);
}
public double to(Unit unit) {
switch (unit) {
case KMH:
public double to(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKMH();
case MPH:
case IMPERIAL:
return toMPH();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KMH,
MPH,
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -4,7 +4,10 @@ import android.content.Context;
import android.content.res.Resources;
import android.util.Pair;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class SpeedFormatter {
@@ -13,14 +16,14 @@ public class SpeedFormatter {
private final int decimalCount;
private final boolean metricUnits;
private final UnitSystem unitSystem;
private final boolean reportSpeedOrPace;
private SpeedFormatter(Resources resources, int decimalCount, boolean metricUnits, boolean reportSpeedOrPace) {
private SpeedFormatter(Resources resources, int decimalCount, UnitSystem unitSystem, boolean reportSpeedOrPace) {
this.resources = resources;
this.decimalCount = decimalCount;
this.metricUnits = metricUnits;
this.unitSystem = unitSystem;
this.reportSpeedOrPace = reportSpeedOrPace;
}
@@ -37,11 +40,17 @@ public class SpeedFormatter {
*/
public Pair<String, String> getSpeedParts(Speed speed) {
int unitId;
if (metricUnits) {
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
} else {
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
switch (unitSystem) {
case METRIC:
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
break;
case IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
break;
default:
throw new RuntimeException("Not implemented");
}
String unitString = resources.getString(unitId);
if (speed == null) {
@@ -49,10 +58,10 @@ public class SpeedFormatter {
}
if (reportSpeedOrPace) {
return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), 1), unitString);
}
int pace = (int) speed.toPace(metricUnits).getSeconds();
int pace = (int) speed.toPace(unitSystem).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
@@ -67,13 +76,12 @@ public class SpeedFormatter {
private int decimalCount;
private boolean metricUnits;
private UnitSystem unitSystem;
private boolean reportSpeedOrPace;
public Builder() {
decimalCount = 2;
metricUnits = true;
reportSpeedOrPace = true;
}
@@ -82,8 +90,8 @@ public class SpeedFormatter {
return this;
}
public Builder setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
public Builder setUnit(@NonNull UnitSystem unitSystem) {
this.unitSystem = unitSystem;
return this;
}
@@ -93,7 +101,7 @@ public class SpeedFormatter {
}
public SpeedFormatter build(Resources resource) {
return new SpeedFormatter(resource, decimalCount, metricUnits, reportSpeedOrPace);
return new SpeedFormatter(resource, decimalCount, unitSystem, reportSpeedOrPace);
}
public SpeedFormatter build(Context context) {
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -81,7 +82,7 @@ public class StatisticsRecordedFragment extends Fragment {
private StatisticsRecordedBinding viewBinding;
private boolean preferenceMetricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -89,7 +90,7 @@ public class StatisticsRecordedFragment extends Fragment {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
@@ -189,7 +190,7 @@ public class StatisticsRecordedFragment extends Fragment {
// Set total distance
{
Pair<String, String> parts = DistanceFormatter.Builder()
.setMetricUnits(preferenceMetricUnits)
.setUnit(unitSystem)
.build(getContext()).getDistanceParts(trackStatistics.getTotalDistance());
viewBinding.statsDistanceValue.setText(parts.first);
@@ -208,7 +209,7 @@ public class StatisticsRecordedFragment extends Fragment {
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
}
SpeedFormatter formatter = SpeedFormatter.Builder().setMetricUnits(preferenceMetricUnits).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
// Set average speed/pace
{
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
@@ -243,11 +244,11 @@ public class StatisticsRecordedFragment extends Fragment {
Pair<String, String> parts;
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeGain_m, preferenceMetricUnits);
parts = StringUtils.getAltitudeParts(getContext(), altitudeGain_m, unitSystem);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, unitSystem);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
@@ -55,14 +56,14 @@ public class StatisticsRecordingFragment extends Fragment {
private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData;
private boolean preferenceMetricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
@@ -157,7 +158,7 @@ public class StatisticsRecordingFragment extends Fragment {
private void updateUI() {
if (isResumed()) {
viewModel.update(recordingData, layout, preferenceMetricUnits);
viewModel.update(recordingData, layout, unitSystem);
}
}
@@ -24,6 +24,7 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -42,7 +43,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
private boolean previousLocationWasAccurate = true;
private Boolean metricUnits = null;
private UnitSystem unitSystem = null;
TrackRecordingServiceNotificationManager(Context context) {
PreferencesUtils.registerOnSharedPreferenceChangeListener(this);
@@ -85,7 +86,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
String formattedAccuracy = context.getString(R.string.value_none);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(context);
if (trackPoint.hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
@@ -97,7 +98,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
}
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
String formattedSpeed = SpeedFormatter.Builder().setMetricUnits(metricUnits).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
@@ -149,8 +150,8 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
notificationManager.cancel(NOTIFICATION_ID);
}
void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
void setUnitSystem(UnitSystem unitSystem) {
this.unitSystem = unitSystem;
}
private void updateNotification() {
@@ -164,7 +165,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
setMetricUnits(PreferencesUtils.isMetricUnits());
setUnitSystem(PreferencesUtils.getUnitSystem());
}
}
}
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -165,12 +166,12 @@ public class VoiceAnnouncement {
}
boolean isMetricUnits = PreferencesUtils.isMetricUnits();
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
//TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits));
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
@@ -178,7 +179,7 @@ public class VoiceAnnouncement {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval, sensorStatistics);
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
@@ -16,6 +16,7 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -25,15 +26,37 @@ class VoiceAnnouncementUtils {
private VoiceAnnouncementUtils() {
}
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
SpannableStringBuilder builder = new SpannableStringBuilder();
Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
int perUnitStringId = isMetricUnits ? R.string.voice_per_kilometer : R.string.voice_per_mile;
double distanceInUnit = distance.toKM_Miles(isMetricUnits);
int distanceId = isMetricUnits ? R.plurals.voiceDistanceKilometers : R.plurals.voiceDistanceMiles;
int perUnitStringId;
int distanceId;
int speedId;
String unitDistanceTTS;
String unitSpeedTTS;
switch (unitSystem) {
case METRIC:
perUnitStringId = R.string.voice_per_kilometer;
distanceId = R.plurals.voiceDistanceKilometers;
speedId = R.plurals.voiceSpeedKilometersPerHour;
unitDistanceTTS = "kilometer";
unitSpeedTTS = "kilometer per hour";
break;
case IMPERIAL:
perUnitStringId = R.string.voice_per_mile;
distanceId = R.plurals.voiceDistanceMiles;
speedId = R.plurals.voiceSpeedMilesPerHour;
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
break;
default:
throw new RuntimeException("Not implemented");
}
double distanceInUnit = distance.toKM_Miles(unitSystem);
builder.append(context.getString(R.string.total_distance));
long distanceIntegerPart = (long) distanceInUnit;
@@ -41,7 +64,7 @@ class VoiceAnnouncementUtils {
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, isMetricUnits ? "kilometer" : "mile");
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
if (distance.isZero()) {
@@ -56,19 +79,18 @@ class VoiceAnnouncementUtils {
}
if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
double speedInUnit = distancePerTime.to(isMetricUnits);
double speedInUnit = distancePerTime.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
long speedIntegerPart = (long) speedInUnit;
// Extract the decimal part
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
builder.append(".");
if (currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(isMetricUnits);
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
@@ -77,12 +99,12 @@ class VoiceAnnouncementUtils {
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
// Extract the decimal part
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
builder.append(".");
}
}
} else {
Duration time = distancePerTime.toPace(isMetricUnits);
Duration time = distancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
@@ -90,7 +112,7 @@ class VoiceAnnouncementUtils {
.append(context.getString(perUnitStringId))
.append(".");
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(isMetricUnits) : Duration.ofMillis(0);
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
if (!currentTime.isZero()) {
builder.append(" ")
.append(context.getString(R.string.lap_time));
@@ -64,10 +64,23 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
}
private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits();
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
int entriesId;
switch (unitSystem) {
case METRIC:
entriesId = R.array.stats_rate_metric_options;
break;
case IMPERIAL:
entriesId = R.array.stats_rate_imperial_options;
break;
default:
throw new RuntimeException("Not implemented");
}
String[] entries = getResources().getStringArray(entriesId);
statsRatePreferences.setEntries(entries);
HackUtils.invalidatePreference(statsRatePreferences);
@@ -17,11 +17,11 @@ public class GpsSettingsFragment extends PreferenceFragmentCompat {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_gps);
boolean metricUnits = PreferencesUtils.isMetricUnits();
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
final DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(getContext());
findPreference(getString(R.string.recording_distance_interval_key))
@@ -32,7 +32,9 @@ import androidx.preference.PreferenceManager;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -193,19 +195,30 @@ public class PreferencesUtils {
return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default));
}
public static boolean isMetricUnits() {
final String STATS_UNIT = resources.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(R.string.stats_units_key, STATS_UNIT));
public static UnitSystem getUnitSystem() {
final String STATS_UNIT_DEFAULT = resources.getString(R.string.stats_units_default);
final String VALUE = getString(R.string.stats_units_key, STATS_UNIT_DEFAULT);
return Arrays.stream(UnitSystem.values())
.filter(d -> VALUE.equals(getString(d.getPreferenceId(), STATS_UNIT_DEFAULT)))
.findFirst()
.orElse(UnitSystem.defaultUnitSystem()); //TODO This AGAIN defines the default
}
public static void setMetricUnits(boolean metricUnits) {
String unit;
if (metricUnits) {
unit = resources.getString(R.string.stats_units_metric);
} else {
unit = resources.getString(R.string.stats_units_imperial);
public static void setUnit(UnitSystem unitSystem) {
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
}
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
public static void applyDefaultUnit() {
if (getString(R.string.stats_units_key, "").equals("")) {
if (!Locale.US.equals(Locale.getDefault())) {
setUnit(UnitSystem.METRIC);
} else {
setUnit(UnitSystem.IMPERIAL);
}
}
setString(R.string.stats_units_key, unit);
}
public static boolean isReportSpeed(String category) {
@@ -292,31 +305,31 @@ public class PreferencesUtils {
}
/**
* @return Result depends on isMetricUnits
* @return Result depends on getUnitSystem
*/
public static Distance getVoiceAnnouncementDistance() {
final float DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_distance_default));
float value = getFloat(R.string.voice_announcement_distance_key, DEFAULT);
return Distance.one(isMetricUnits()).multipliedBy(value);
return Distance.one(getUnitSystem()).multipliedBy(value);
}
/**
* @return Result depends on isMetricUnits
* @return Result depends on getUnitSystem
*/
static String[] getVoiceAnnouncementDistanceEntries() {
String[] values = resources.getStringArray(R.array.voice_announcement_distance_values);
String[] options = new String[values.length];
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < values.length; i++) {
if (resources.getString(R.string.announcement_off).equals(values[i])) {
options[i] = resources.getString(R.string.value_off);
} else {
Distance distance = Distance.one(metricUnits).multipliedBy(Double.parseDouble(values[i]));
Distance distance = Distance.one(unitSystem).multipliedBy(Double.parseDouble(values[i]));
options[i] = formatter.formatDistance(distance);
}
}
@@ -354,10 +367,10 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = (int) getRecordingDistanceIntervalDefault().toM();
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.setDecimalCount(0)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
@@ -365,18 +378,23 @@ public class PreferencesUtils {
Distance distance = Distance.of(1).multipliedBy(value);
String displayValue = formatter.formatDistance(distance);
if (metricUnits) {
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
} else {
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
switch (unitSystem) {
case METRIC:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -393,29 +411,34 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
Distance distance = Distance.of(1).multipliedBy(value);
String displayValue = formatter.formatDistance(distance);
if (metricUnits) {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
} else {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
switch (unitSystem) {
case METRIC:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -460,11 +483,11 @@ public class PreferencesUtils {
final int recordingGPSAccuracyExcellent = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_excellent));
final int recordingGPSAccuracyPoor = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_poor));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
@@ -472,24 +495,29 @@ public class PreferencesUtils {
Distance distance = Distance.of(1).multipliedBy(value);
String displayValue = formatter.formatDistance(distance);
if (metricUnits) {
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
} else if (value == recordingGPSAccuracyPoor) {
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
} else {
entries[i] = displayValue;
}
} else {
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
switch (unitSystem) {
case METRIC:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
} else if (value == recordingGPSAccuracyPoor) {
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -509,24 +537,30 @@ public class PreferencesUtils {
final float idleSpeedDefault = Float.parseFloat(resources.getString(R.string.idle_speed_default));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
for (int i = 0; i < entryValues.length; i++) {
float value = Float.parseFloat(entryValues[i]);
if (metricUnits) {
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
}
} else {
double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
switch (unitSystem) {
case METRIC:
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
}
break;
case IMPERIAL:
double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.settings;
import de.dennisguse.opentracks.R;
public enum UnitSystem {
METRIC(R.string.stats_units_metric),
IMPERIAL(R.string.stats_units_imperial);
private final int preferenceIdentifier;
UnitSystem(int preferenceIdentifier) {
this.preferenceIdentifier = preferenceIdentifier;
}
public int getPreferenceId() {
return preferenceIdentifier;
}
@Deprecated //TODO used to initialize before loading from preferences; should be loaded first
public static UnitSystem defaultUnitSystem() {
return METRIC;
}
}
@@ -31,6 +31,7 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -210,8 +211,8 @@ public class DescriptionGenerator {
*/
@VisibleForTesting
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setMetricUnits(true).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
Pair<String, String> paceInImperial = SpeedFormatter.Builder().setMetricUnits(false).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setUnit(UnitSystem.METRIC).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
Pair<String, String> paceInImperial = SpeedFormatter.Builder().setUnit(UnitSystem.IMPERIAL).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
@@ -16,6 +16,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -99,7 +100,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
private final TextView maxSpeedUnit;
private final TextView maxSpeedLabel;
private boolean metricsUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed;
public ViewHolder(View view) {
@@ -121,7 +122,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
SpeedFormatter formatter = SpeedFormatter.Builder().setMetricUnits(metricsUnits).setReportSpeedOrPace(reportSpeed).build(context);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
{
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed());
avgSpeed.setText(parts.first);
@@ -138,7 +139,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
}
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
SpeedFormatter formatter = SpeedFormatter.Builder().setMetricUnits(metricsUnits).setReportSpeedOrPace(reportSpeed).build(context);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
setCommonValues(aggregatedStatistic);
{
@@ -161,14 +162,14 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(category);
metricsUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
Pair<String, String> parts = DistanceFormatter.Builder()
.setMetricUnits(metricsUnits)
.setUnit(unitSystem)
.build(context).getDistanceParts(aggregatedStatistic.getTrackStatistics().getTotalDistance());
distance.setText(parts.first);
distanceUnit.setText(parts.second);
@@ -15,6 +15,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -22,11 +23,11 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
private List<IntervalStatistics.Interval> intervalList;
private final Context context;
private final StackMode stackMode;
private boolean metricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean isReportSpeed;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
this.metricUnits = metricUnits;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, UnitSystem unitSystem, boolean isReportSpeed) {
this.unitSystem = unitSystem;
this.context = context;
this.stackMode = stackMode;
this.isReportSpeed = isReportSpeed;
@@ -57,14 +58,14 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
sumDistance = interval.getDistance().multipliedBy(nextPosition);
}
viewHolder.distance.setText(DistanceFormatter.Builder()
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(context).formatDistance(sumDistance));
SpeedFormatter formatter = SpeedFormatter.Builder().setMetricUnits(metricUnits).setReportSpeedOrPace(isReportSpeed).build(context);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(isReportSpeed).build(context);
viewHolder.rate.setText(formatter.formatSpeed(interval.getSpeed()));
viewHolder.gain.setText(StringUtils.formatAltitudeChange(context, interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatAltitudeChange(context, interval.getLoss_m(), metricUnits));
viewHolder.gain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), unitSystem));
viewHolder.loss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), unitSystem));
}
@@ -76,8 +77,8 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
return intervalList.size();
}
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
this.metricUnits = metricUnits;
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, UnitSystem unitSystem, boolean isReportSpeed) {
this.unitSystem = unitSystem;
this.isReportSpeed = isReportSpeed;
intervalList = data;
@@ -22,6 +22,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* This model is used to load intervals for a track.
@@ -64,14 +65,14 @@ public class IntervalStatisticsModel extends AndroidViewModel {
handler = null;
}
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
if (intervalsLiveData == null) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
intervalsLiveData = new MutableLiveData<>();
distanceInterval = interval.getDistance(metricUnits);
distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId);
@@ -104,13 +105,13 @@ public class IntervalStatisticsModel extends AndroidViewModel {
}
}
public void update(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
public void update(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
if (interval == null) {
interval = IntervalOption.DEFAULT;
}
lastTrackPointId = null;
distanceInterval = interval.getDistance(metricUnits);
distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId);
}
@@ -138,9 +139,9 @@ public class IntervalStatisticsModel extends AndroidViewModel {
this.multiplier = multiplier;
}
public Distance getDistance(boolean metricUnits) {
public Distance getDistance(UnitSystem unitSystem) {
return Distance
.one(metricUnits)
.one(unitSystem)
.multipliedBy(multiplier);
}
@@ -24,6 +24,7 @@ import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* A fragment to display the intervals from recorded track.
@@ -41,7 +42,7 @@ public class IntervalsFragment extends Fragment {
private IntervalStatisticsModel.IntervalOption selectedInterval;
private Track.Id trackId;
private boolean metricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private IntervalStatisticsAdapter adapter;
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> intervalsAdapter;
@@ -51,7 +52,7 @@ public class IntervalsFragment extends Fragment {
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
updateIntervals(PreferencesUtils.isMetricUnits(), selectedInterval);
updateIntervals(PreferencesUtils.getUnitSystem(), selectedInterval);
if (intervalsAdapter != null) {
intervalsAdapter.notifyDataSetChanged();
}
@@ -101,14 +102,14 @@ public class IntervalsFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, unitSystem, isReportSpeed);
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
final DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(getContext());
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@@ -118,7 +119,7 @@ public class IntervalsFragment extends Fragment {
TextView v = (TextView) super.getView(position, convertView, parent);
IntervalStatisticsModel.IntervalOption option = getItem(position);
String stringValue = formatter.formatDistance(option.getDistance(metricUnits));
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
v.setText(stringValue);
return v;
}
@@ -131,7 +132,7 @@ public class IntervalsFragment extends Fragment {
viewBinding.intervalsDropdown.setAdapter(intervalsAdapter);
viewBinding.intervalsDropdown.setOnItemClickListener((parent, view1, position, id) -> {
updateIntervals(metricUnits, IntervalStatisticsModel.IntervalOption.values()[position]);
updateIntervals(unitSystem, IntervalStatisticsModel.IntervalOption.values()[position]);
//TODO This duplicates the intervalAdapter code
setIntervalsDropdownText();
@@ -191,8 +192,8 @@ public class IntervalsFragment extends Fragment {
}
viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace));
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, metricUnits, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, unitSystem, isReportSpeed));
setIntervalsDropdownText();
}
@@ -200,23 +201,23 @@ public class IntervalsFragment extends Fragment {
private void setIntervalsDropdownText() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(getContext());
IntervalStatisticsModel.IntervalOption option = selectedInterval != null ? selectedInterval : IntervalStatisticsModel.IntervalOption.DEFAULT;
String stringValue = formatter.formatDistance(option.getDistance(metricUnits));
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
viewBinding.intervalsDropdown.setText(stringValue, false);
}
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = metricUnits != this.metricUnits
private synchronized void updateIntervals(UnitSystem unitSystem, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = unitSystem != this.unitSystem
|| selectedInterval == null
|| !selectedInterval.sameMultiplier(this.selectedInterval);
this.metricUnits = metricUnits;
this.unitSystem = unitSystem;
this.selectedInterval = selectedInterval;
if (update && viewModel != null) {
viewModel.update(trackId, this.metricUnits, this.selectedInterval);
viewModel.update(trackId, this.unitSystem, this.selectedInterval);
}
}
}
@@ -43,8 +43,10 @@ import java.time.temporal.TemporalAccessor;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* Various string manipulation methods.
@@ -252,35 +254,18 @@ public class StringUtils {
/**
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, boolean metricUnits) {
String formattedValue = context.getString(R.string.value_unknown);
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(1)
.setUnit(unitSystem)
.build(context);
if (altitude_m != null) {
double value = Distance.of(altitude_m).toM_FT(metricUnits);
formattedValue = StringUtils.formatDecimal(value, 1);
}
return new Pair<>(formattedValue, unit);
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
return formatter.getDistanceParts(distance);
}
public static String formatAltitude(Context context, Float altitude_m, boolean metricUnits) {
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, metricUnits);
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
}
/**
* @return the formatted altitudeDifference_m (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getAltitudeChangeParts(Context context, Float altitudeDifference_m, boolean metricUnits) {
String formattedValue = altitudeDifference_m != null ? String.valueOf((int) Distance.of(altitudeDifference_m).toM_FT(metricUnits)) : context.getString(R.string.value_unknown);
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
return new Pair<>(formattedValue, unit);
}
public static String formatAltitudeChange(Context context, Float altitude_m, boolean metricUnits) {
Pair<String, String> altitudeParts = getAltitudeChangeParts(context, altitude_m, metricUnits);
public static String formatAltitude(Context context, Float altitude_m, UnitSystem unitSystem) {
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, unitSystem);
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
}
@@ -18,6 +18,7 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
@@ -26,17 +27,17 @@ import de.dennisguse.opentracks.util.StringUtils;
public class StatisticDataBuilder {
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, boolean metricUnits) {
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, UnitSystem unitSystem) {
List<StatisticData> statisticDataList = layout.getFields().stream()
.filter(DataField::isVisible)
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), metricUnits))
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), unitSystem))
.filter(Objects::nonNull)
.collect(Collectors.toList());
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, metricUnits));
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, unitSystem));
return statisticDataList;
}
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, boolean metricUnits) {
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, UnitSystem unitSystem) {
final TrackPoint latestTrackPoint = recordingData.getLatestTrackPoint();
final SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
final TrackStatistics trackStatistics = recordingData.getTrackStatistics();
@@ -48,8 +49,8 @@ public class StatisticDataBuilder {
final String sensorUnknown = context.getString(R.string.value_unknown);
SpeedFormatter speedFormatterPace = SpeedFormatter.Builder().setMetricUnits(metricUnits).setReportSpeedOrPace(false).build(context);
SpeedFormatter speedFormatterSpeed = SpeedFormatter.Builder().setMetricUnits(metricUnits).setReportSpeedOrPace(true).build(context);
SpeedFormatter speedFormatterPace = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(false).build(context);
SpeedFormatter speedFormatterSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context);
if (fieldKey.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
@@ -60,14 +61,14 @@ public class StatisticDataBuilder {
title = context.getString(R.string.stats_moving_time);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
valueAndUnit = DistanceFormatter.Builder()
.setMetricUnits(metricUnits)
.setUnit(unitSystem)
.build(context).getDistanceParts(trackStatistics.getTotalDistance());
title = context.getString(R.string.stats_distance);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_speed_key)) || fieldKey.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
boolean reportSpeed = fieldKey.equals("speed");
title = reportSpeed ? context.getString(R.string.stats_speed) : context.getString(R.string.stats_pace);
SpeedFormatter localSpeedFormatter = SpeedFormatter.Builder().setMetricUnits(metricUnits).setReportSpeedOrPace(reportSpeed).build(context);
SpeedFormatter localSpeedFormatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
if (sensorDataSet != null && sensorDataSet.getSpeed() != null) {
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.getSpeed().first);
description = sensorDataSet.getSpeed().second;
@@ -98,12 +99,12 @@ public class StatisticDataBuilder {
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
title = context.getString(R.string.stats_altitude);
description = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? context.getString(latestTrackPoint.getAltitude().getLabelId()) : null;
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, unitSystem);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeGain(), metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeGain(), unitSystem);
title = context.getString(R.string.stats_gain);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeLoss(), metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeLoss(), unitSystem);
title = context.getString(R.string.stats_loss);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
title = context.getString(R.string.stats_coordinates);
@@ -162,20 +163,20 @@ public class StatisticDataBuilder {
/**
* Builds a list of StatisticData with sensors configured but not in statisticDataList.
*/
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, boolean metricUnits) {
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, UnitSystem unitSystem) {
List<StatisticData> sensorDataList = new ArrayList<>();
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet == null) {
return sensorDataList;
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))) && sensorDataSet.getHeartRate() != null) {
sensorDataList.add(build(context, recordingData, "heart_rate", true, metricUnits));
sensorDataList.add(build(context, recordingData, "heart_rate", true, unitSystem));
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))) && sensorDataSet.getCadence() != null) {
sensorDataList.add(build(context, recordingData, "cadence", true, metricUnits));
sensorDataList.add(build(context, recordingData, "cadence", true, unitSystem));
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))) && sensorDataSet.getCyclingPower() != null && sensorDataSet.getCyclingPower().hasValue()) {
sensorDataList.add(build(context, recordingData, "power", true, metricUnits));
sensorDataList.add(build(context, recordingData, "power", true, unitSystem));
}
return sensorDataList;
@@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
public class StatisticsDataModel extends AndroidViewModel {
@@ -26,9 +27,9 @@ public class StatisticsDataModel extends AndroidViewModel {
return statsData;
}
public void update(RecordingData recordingData, Layout layout, boolean metricUnit) {
public void update(RecordingData recordingData, Layout layout, UnitSystem unitSystem) {
new Thread(() -> {
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, unitSystem);
statsData.postValue(statisticDataList);
}).start();
}