Compare commits

..

12 Commits

Author SHA1 Message Date
Dennis Guse ae2a97576b Release: v4.0.2 2022-06-20 22:06:01 +02:00
Šimon Hořánek 3e830a086e Replace Start with Stop 2022-06-19 21:03:56 +02:00
Surfoo 618a1df3ea Translated using Weblate (French)
Currently translated at 82.9% (73 of 88 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/fr/
2022-06-19 17:34:42 +02:00
Surfoo 90cfc67c59 Translated using Weblate (French)
Currently translated at 100.0% (487 of 487 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-06-19 17:34:42 +02:00
Dennis Guse cecc586868 Bugfix: export was not checking properly if file exists.
Fixes #1283.
2022-06-19 17:24:18 +02:00
Dennis Guse f956e7fab3 Duplicate variable name. 2022-06-19 17:21:56 +02:00
William Pettersson 128b95cd83 Fix voice announcement rounding problem
We extracted integral parts before rounding, but String.format does
rounding which caused occasional issues. To alleviate, we need to round
before passing to String.format. All this rounding logic has now been
moved into appendDecimalUnit, and tests have been added to check that
rounding works correctly.

Fixes: #1285
2022-06-19 16:39:46 +02:00
Román Ginés 895a5c2b2a Bugfixed: DistanceFormatter was using the wrong unitSystem. Fixes #1288. 2022-06-18 23:18:04 +02:00
Dennis Guse 91ce5cf8d7 Add nautical miles (with feet for altitude).
Tests were broken.

Fixes #1280.
2022-06-18 10:01:09 +02:00
Dennis Guse a283464331 Add nautical miles (with feet for altitude).
Fixes #1280.
2022-06-17 19:57:40 +02:00
Dennis Guse 1c3a2bd2e4 Removed altitude smoothing completely: now it will just be the plain GPS-reported value.
Was only used for statistics in exports.

Follow-up to #1241.
2022-06-11 14:41:56 +02:00
Dennis Guse 0b326a77c2 Bugfix: Elevation in charts smoothing removed as it was broken.
Fixes #1241.
2022-06-11 14:24:41 +02:00
35 changed files with 282 additions and 215 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ The API can be invoked by sending an explicit Intent to start an activity.
`Class`:
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
For testing via adb: `adb shell am start -n "package/class"`
+2 -2
View File
@@ -59,8 +59,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4926
versionName "v4.0.1"
versionCode 4938
versionName "v4.0.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -0,0 +1,10 @@
v4.0.2: OpenTracks
Changes:
- Add nautical miles
- Settings can be reset while recording
Bugfix:
- Voice announcement rounding
- Chart: elevation was weirdly smoothed
- Export was not checking if file exists
@@ -0,0 +1,11 @@
v3.20.2 : OpenTracks
Changements :
- Le gain et la perte d'altitude sont des entiers au lieu de floats
- Demander la permission avant d'utiliser Bluetooth par un message Toast
Correction d'un bug :
- Définir FLAG_IMMUTABLE pour PendingIntents pour éviter un crash sur Android 12
- Le capteur de cadence renvoie 0 au lieu de None lorsque l'utilisateur ne pédale pas.
- Le capteur de vitesse renvoie 0 au lieu de la dernière vitesse connue quand c'est arrêté
- Avec les capteurs, le temps de déplacement s'arrête lorsqu'il n'est pas en mouvement
@@ -7,5 +7,11 @@ Fonctionnalités :
* Dénivelé positif et négatif: via capteur de pression barométrique
* Synthèse vocale
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
* Thème sombre ou claire, avec le respect des paramètres du système
* Pas de publicités
Logiciel Open Source
Vous pouvez utiliser, étudier, changer et partager le code source.
Licence Apache 2.0
@@ -27,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, UnitSystem.IMPERIAL);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -40,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, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), true, UnitSystem.METRIC);
// then
assertEquals(1, (long) point.getTimeOrDistance());
@@ -50,9 +50,11 @@ public class ChartPointTest {
public void create_get_altitude_speed_and_pace() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(50, point.getAltitude(), 0.01);
@@ -64,9 +66,10 @@ public class ChartPointTest {
public void create_sensorNotAvailable() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertNull(point.getHeartRate());
@@ -77,15 +80,16 @@ public class ChartPointTest {
@Test
public void create_sensorAvailable() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setHeartRate(100f);
trackPoint.setCadence(101f);
trackPoint.setPower(102f);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50))
.setHeartRate(100f)
.setCadence(101f)
.setPower(102f);
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
@@ -47,6 +47,7 @@ public class ChartValueSeriesTest {
new int[]{1, 100, 1000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
15,
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
//TODO Parametrized tests
@RunWith(AndroidJUnit4.class)
public class DistanceFormatterTest {
@@ -42,4 +43,17 @@ public class DistanceFormatterTest {
// A small number in imperial
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
}
@Test
public void testFormatDistance_nautical() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.build(context);
// A large number in nautical
assertEquals("2.70 NM", formatter.formatDistance(Distance.of(5000)));
// A small number in nautical
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
}
}
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
//TODO Use make parametrized tests?
@RunWith(AndroidJUnit4.class)
public class SpeedFormatterTest {
@@ -39,12 +40,22 @@ public class SpeedFormatterTest {
.setReportSpeedOrPace(false)
.build(context);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
assertEquals("8:02", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_pace_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(false)
.build(context);
assertEquals("9:14", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("min/NM", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
@@ -69,6 +80,18 @@ public class SpeedFormatterTest {
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(true)
.build(context);
assertEquals("6.5", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("kn", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
@@ -67,6 +67,54 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_speed_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(19999));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check_two() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(19990));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_withInterval_metric_speed() {
// given
@@ -86,7 +86,7 @@ public class TrackStatisticsUpdaterTest {
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(28.75, statistics.getMaxAltitude(), 0.01);
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
@@ -1,63 +0,0 @@
package de.dennisguse.opentracks.util;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RunWith(JUnit4.class)
public class ExportUtilsTest extends TestCase {
@Test
public void testIsExportFileExists_exists() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(uuid.toString().substring(0, 8) + "_other name name.kmz");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertTrue(exists);
}
@Test
public void testIsExportFileExists_not_exists1() {
// given
UUID uuid = UUID.randomUUID();
String format = "gpx";
List<String> fileNames = new ArrayList<>();
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
@Test
public void testIsExportFileExists_not_exists2() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
}
@@ -188,9 +188,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
}
}
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -29,15 +29,15 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
if (trackPoint.hasAltitude()) {
altitude = Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem);
}
if (smoothedSpeed != null) {
@@ -44,6 +44,7 @@ abstract class ChartValueSeries {
private final int[] intervalValues;
private final int metricTitleId;
private final int imperialTitleId;
private final int nauticalTitleId;
private final Paint fillPaint;
private final Paint strokePaint;
private final Paint titlePaint;
@@ -57,24 +58,13 @@ abstract class ChartValueSeries {
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
private boolean enabled = true;
/**
* Constructor.
*
* @param context the context
* @param absoluteMin the absolute min value
* @param absoluteMax the absolute max value
* @param intervalValues the list of interval values
* @param metricTitleId the metric title id
* @param imperialTitleId the imperial title id
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int nauticalTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
this.metricTitleId = metricTitleId;
this.imperialTitleId = imperialTitleId;
this.nauticalTitleId = nauticalTitleId;
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
@@ -224,39 +214,27 @@ abstract class ChartValueSeries {
return metricTitleId;
case IMPERIAL:
return imperialTitleId;
case NAUTICAL_IMPERIAL:
return nauticalTitleId;
default:
throw new RuntimeException("Not implemented");
}
}
/**
* Gets the title paint.
*/
Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
}
/**
* Formats a marker value.
*
* @param value the value
*/
String formatMarker(int value) {
return numberFormat.format(value);
}
@@ -203,6 +203,7 @@ public class ChartView extends View {
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
fontSizeSmall,
@@ -224,6 +225,7 @@ public class ChartView extends View {
new int[]{1, 5, 10, 20, 50, 100},
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.string.description_speed_nautical,
R.color.chart_speed_fill,
R.color.chart_speed_border,
fontSizeSmall,
@@ -246,6 +248,7 @@ public class ChartView extends View {
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.string.description_pace_nautical,
R.color.chart_pace_fill,
R.color.chart_pace_border,
fontSizeSmall,
@@ -268,6 +271,7 @@ public class ChartView extends View {
new int[]{25, 50},
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border,
fontSizeSmall,
@@ -289,6 +293,7 @@ public class ChartView extends View {
new int[]{5, 10, 25, 50},
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border,
fontSizeSmall,
@@ -309,6 +314,7 @@ public class ChartView extends View {
new int[]{5, 50, 100, 200},
R.string.description_sensor_power,
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border,
fontSizeSmall,
@@ -732,6 +738,8 @@ public class ChartView extends View {
return context.getString(R.string.unit_kilometer);
case IMPERIAL:
return context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL:
return context.getString(R.string.unit_nautical_mile);
default:
throw new RuntimeException("Not implemented");
}
@@ -367,7 +367,7 @@ public class TrackDataHub {
// Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
for (Listener trackDataListener : listeners) {
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed());
}
} else {
for (Listener trackDataListener : listeners) {
@@ -432,7 +432,7 @@ public class TrackDataHub {
*
* @param trackPoint the trackPoint
*/
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed) {
}
/**
@@ -29,6 +29,10 @@ public class Distance {
return of(distance_mile * UnitConversions.MI_TO_M);
}
public static Distance ofNauticalMile(double distance_mile) {
return of(distance_mile * UnitConversions.NAUTICAL_MILE_TO_M);
}
public static Distance ofKilometer(double distance_km) {
return of(distance_km * UnitConversions.KM_TO_M);
}
@@ -51,6 +55,8 @@ public class Distance {
return Distance.ofKilometer(1);
case IMPERIAL:
return Distance.ofMile(1);
case NAUTICAL_IMPERIAL:
return Distance.ofNauticalMile(1);
default:
throw new RuntimeException("Not implemented");
}
@@ -114,12 +120,18 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI;
}
public double toNauticalMiles() {
return toKM() * UnitConversions.KM_TO_NAUTICAL_MILE;
}
public double toKM_Miles(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKM();
case IMPERIAL:
return toMI();
case NAUTICAL_IMPERIAL:
return toNauticalMiles();
default:
throw new RuntimeException("Not implemented");
}
@@ -129,6 +141,7 @@ public class Distance {
switch (unitSystem) {
case METRIC:
return toM();
case NAUTICAL_IMPERIAL:
case IMPERIAL:
return toFT();
default:
@@ -46,6 +46,7 @@ public class DistanceFormatter {
case METRIC:
return new Pair<>(null, resources.getString(R.string.unit_meter));
case IMPERIAL:
case NAUTICAL_IMPERIAL:
return new Pair<>(null, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
@@ -65,6 +66,12 @@ public class DistanceFormatter {
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
case NAUTICAL_IMPERIAL:
if (distance.greaterThan(Distance.ofNauticalMile(0.5))) {
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
default:
throw new RuntimeException("Not implemented");
}
@@ -93,6 +93,10 @@ public class Speed {
return toKMH() * UnitConversions.KM_TO_MI;
}
public double toKnots() {
return toKMH() * UnitConversions.KM_TO_NAUTICAL_MILE;
}
public Duration toPace(UnitSystem unitSystem) {
if (isZero()) {
return Duration.ofSeconds(0);
@@ -106,6 +110,9 @@ public class Speed {
case IMPERIAL:
distance *= UnitConversions.M_TO_MI;
break;
case NAUTICAL_IMPERIAL:
distance *= UnitConversions.M_TO_NAUTICAL_MILE;
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -119,6 +126,8 @@ public class Speed {
return toKMH();
case IMPERIAL:
return toMPH();
case NAUTICAL_IMPERIAL:
return toKnots();
default:
throw new RuntimeException("Not implemented");
}
@@ -47,6 +47,9 @@ public class SpeedFormatter {
case IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
break;
case NAUTICAL_IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -46,7 +46,7 @@ public class TrackPoint {
private Double longitude;
private Distance horizontalAccuracy;
private Distance verticalAccuracy;
private Altitude altitude;
private Altitude altitude; //TODO use Altitude.WGS84
private Speed speed;
private Float bearing;
private Distance sensorDistance;
@@ -53,6 +53,9 @@ public class UnitConversions {
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
// multiplication factor to convert meters to feet
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
public static final double NAUTICAL_MILE_TO_M = 1852.0;
public static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
public static final double KM_TO_NAUTICAL_MILE = 1000 * M_TO_NAUTICAL_MILE;
// multiplication factor to convert meters per second to kilometers per hour
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
// multiplication factor to convert miles to km
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -234,7 +235,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* @param conflictResolution conflict resolution to be applied if needed.
*/
private void export(Track track, int conflictResolution) {
boolean fileExists = ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles);
boolean fileExists = exportFileExists(track);
if (fileExists && conflictResolution == CONFLICT_NONE) {
conflict(track);
@@ -250,6 +251,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
export(track, autoConflict);
}
private boolean exportFileExists(Track track) {
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
return directoryFiles.stream().anyMatch(filename::equals);
}
private void setConflictVisibility(int visibility) {
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
@@ -315,7 +321,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
public void onExportSuccess(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles)) {
if (exportFileExists(track)) {
trackExportOverwrittenCount++;
} else {
trackExportSuccessCount++;
@@ -57,6 +57,13 @@ class VoiceAnnouncementUtils {
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
break;
case NAUTICAL_IMPERIAL:
perUnitStringId = R.string.voice_per_nautical_mile;
distanceId = R.plurals.voiceDistanceNauticalMiles;
speedId = R.plurals.voiceSpeedMKnots;
unitDistanceTTS = "nautical mile";
unitSpeedTTS = "knots";
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -65,12 +72,9 @@ class VoiceAnnouncementUtils {
if (shouldVoiceAnnounceTotalDistance()) {
builder.append(context.getString(R.string.total_distance));
long distanceIntegerPart = (long) distanceInUnit;
// Extract the decimal part
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, unitDistanceTTS);
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 2, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
@@ -88,27 +92,17 @@ class VoiceAnnouncementUtils {
if (isReportSpeed) {
if (shouldVoiceAnnounceAverageSpeedPace()) {
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, unitSpeedTTS);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
builder.append(" ")
.append(context.getString(R.string.lap_speed));
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, unitSpeedTTS);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
@@ -164,26 +158,34 @@ class VoiceAnnouncementUtils {
int seconds = (int) (duration.getSeconds() % 60);
if (hours > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, null, "hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
}
if (minutes > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, null, "minute");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
}
if (seconds > 0 || duration.isZero()) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, null, "second");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
}
}
/**
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
*
* @param number The number to speak
* @param precision The number of decimal places to announce
*/
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, long integerPart, @Nullable String fractionalPart, @NonNull String unit) {
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, double number, int precision, @NonNull String unit) {
TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder()
.setUnit(unit);
if (fractionalPart == null) {
measureBuilder.setNumber(integerPart);
if (precision == 0) {
measureBuilder.setNumber((long)number);
} else {
// Round before extracting integral and decimal parts
double roundedNumber = Math.round(Math.pow(10, precision) * number) / Math.pow(10.0, precision);
long integerPart = (long) roundedNumber;
// Extract the decimal part
String fractionalPart = String.format("%." + precision + "f", (roundedNumber - integerPart)).substring(2);
measureBuilder.setIntegerPart(integerPart)
.setFractionalPart(fractionalPart);
}
@@ -74,6 +74,7 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
entriesId = R.array.stats_rate_metric_options;
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
entriesId = R.array.stats_rate_imperial_options;
break;
default:
@@ -214,7 +214,6 @@ public class PreferencesUtils {
//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 {
@@ -324,7 +323,7 @@ public class PreferencesUtils {
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setDecimalCount(1)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < values.length; i++) {
@@ -418,7 +417,7 @@ public class PreferencesUtils {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.setDecimalCount(0)
.setDecimalCount(1)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
@@ -434,6 +433,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
@@ -461,7 +461,7 @@ public class PreferencesUtils {
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setDecimalCount(1)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
@@ -478,6 +478,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
@@ -533,7 +534,7 @@ public class PreferencesUtils {
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setDecimalCount(1)
.setUnit(unitSystem)
.build(resources);
@@ -555,6 +556,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) {
@@ -606,6 +608,14 @@ public class PreferencesUtils {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
break;
case NAUTICAL_IMPERIAL:
double valueKnots = Speed.ofKMH(value).toKnots();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
} else {
entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
}
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -4,7 +4,8 @@ import de.dennisguse.opentracks.R;
public enum UnitSystem {
METRIC(R.string.stats_units_metric),
IMPERIAL(R.string.stats_units_imperial);
IMPERIAL(R.string.stats_units_imperial),
NAUTICAL_IMPERIAL(R.string.stats_units_nautical); // Nautical miles with feet
private final int preferenceIdentifier;
@@ -1,32 +0,0 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.data.models.Altitude;
public class AltitudeRingBuffer extends RingBuffer<Altitude> {
private Altitude firstAltitude;
AltitudeRingBuffer(int size) {
super(size);
}
AltitudeRingBuffer(RingBuffer<Altitude> toCopy) {
super(toCopy);
}
@Nullable
@Override
protected Number from(Altitude object) {
if (firstAltitude == null) {
firstAltitude = object;
}
return object.toM();
}
@Override
protected Altitude to(double object) {
return firstAltitude.replace(object);
}
}
@@ -41,12 +41,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
*/
public class TrackStatisticsUpdater {
/**
* The number of altitude readings to smooth to get a somewhat accurate signal.
*/
@VisibleForTesting
private static final int ALTITUDE_SMOOTHING_FACTOR = 25;
/**
* The number of speed reading to smooth to get a somewhat accurate signal.
*/
@@ -63,7 +57,6 @@ public class TrackStatisticsUpdater {
private final TrackStatistics trackStatistics;
private final AltitudeRingBuffer altitudeBuffer;
private final SpeedRingBuffer speedBuffer;
private float averageHeartRateBPM;
private Duration totalHeartRateDuration = Duration.ZERO;
@@ -86,7 +79,6 @@ public class TrackStatisticsUpdater {
this.trackStatistics = trackStatistics;
this.currentSegment = new TrackStatistics();
altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR);
resetAverageHeartRate();
}
@@ -95,7 +87,6 @@ public class TrackStatisticsUpdater {
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
this.altitudeBuffer = new AltitudeRingBuffer(toCopy.altitudeBuffer);
this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer);
this.lastTrackPoint = toCopy.lastTrackPoint;
@@ -140,11 +131,7 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
// Update altitude using the smoothed average
altitudeBuffer.setNext(trackPoint.getAltitude());
Altitude newAverage = altitudeBuffer.getAverage();
currentSegment.updateAltitudeExtremities(newAverage);
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
}
// Update heart rate
@@ -198,7 +185,6 @@ public class TrackStatisticsUpdater {
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
altitudeBuffer.reset();
speedBuffer.reset();
resetAverageHeartRate();
}
@@ -208,14 +194,6 @@ public class TrackStatisticsUpdater {
totalHeartRateDuration = Duration.ZERO;
}
/**
* Gets the smoothed altitude over several readings.
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
*/
public Altitude getSmoothedAltitude() {
return altitudeBuffer.getAverage();
}
public Speed getSmoothedSpeed() {
return speedBuffer.getAverage();
}
@@ -107,17 +107,17 @@ public class IntervalsFragment extends Fragment {
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
final DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(getContext());
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
TextView v = (TextView) super.getView(position, convertView, parent);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(getContext());
IntervalStatisticsModel.IntervalOption option = getItem(position);
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
v.setText(stringValue);
@@ -85,12 +85,6 @@ public class ExportUtils {
}
}
public static boolean isExportFileExists(UUID uuuid, String trackFileFormatExtension, List<String> filesName) {
String word = uuuid.toString().substring(0, 8) + ".*\\." + trackFileFormatExtension;
Pattern pattern = Pattern.compile(word);
return filesName.stream().anyMatch(w -> pattern.matcher(w).matches());
}
public static List<String> getAllFiles(Context context, Uri directoryUri) {
List<String> fileNames = new ArrayList<>();
final ContentResolver resolver = context.getContentResolver();
+6
View File
@@ -581,4 +581,10 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="select_show_on_map_behavior">Sélectionner le comportement \"Montrer sur la carte\"</string>
<string name="settings_public_api_dashboard_enabled_title">Transfert automatique des données</string>
<string name="settings_public_api_dashboard_enabled_summary_on">Une application qui a commencé un enregistrement peut aussi accéder aux données enregistrées.</string>
<string name="settings_announcements_average_speed_pace">Allure/vitesse moyenne</string>
<string name="settings_announcements_lap_speed_pace">Allure/vitesse au tour</string>
<string name="settings_announcements_average_heart_rate">Moyenne de fréquence cardiaque</string>
<string name="settings_announcements_lap_heart_rate">Fréquence cardiaque au tour</string>
<string name="image_discard">Ignorer</string>
<string name="track_discarding">Ignorer la trace</string>
</resources>
+11 -3
View File
@@ -204,15 +204,20 @@
<string name="stats_rate_key" translatable="false">statsRate</string>
<string name="stats_rate_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
<string-array name="stats_rate_metric_options">
<item>@string/description_default_speed_or_pace</item>
<item>@string/description_speed_metric</item>
<item>@string/description_pace_metric</item>
</string-array>
<string-array name="stats_rate_imperial_options">
<item>@string/description_default_speed_or_pace</item>
<item>@string/description_speed_imperial</item>
<item>@string/description_pace_imperial</item>
</string-array>
<string-array name="stats_rate_metric_options">
<string-array name="stats_rate_nautical_options">
<item>@string/description_default_speed_or_pace</item>
<item>@string/description_speed_metric</item>
<item>@string/description_pace_metric</item>
<item>@string/description_speed_nautical</item>
<item>@string/description_pace_imperial</item>
</string-array>
<string name="stats_rate_speed_or_pace_default" translatable="false">ACTIVITY_TYPE</string>
@@ -229,12 +234,15 @@
<string-array name="stats_units_values">
<item>@string/stats_units_metric</item>
<item>@string/stats_units_imperial</item>
<item>@string/stats_units_nautical</item>
</string-array>
<string name="stats_units_metric" translatable="false">METRIC</string>
<string name="stats_units_imperial" translatable="false">IMPERIAL</string>
<string name="stats_units_nautical" translatable="false">NAUTICAL</string>
<string-array name="stats_units_options">
<item>@string/settings_stats_units_metric</item>
<item>@string/settings_stats_units_imperial</item>
<item>@string/settings_stats_units_nautical</item>
</string-array>
<string name="track_name_key" translatable="false">trackName</string>
+18
View File
@@ -187,6 +187,7 @@ limitations under the License.
<string name="description_min_altitude">Min elevation: %1$d m (%2$d ft)</string>
<string name="description_moving_time">Moving time: %1$s</string>
<string name="description_pace_imperial">Pace (min/mi)</string>
<string name="description_pace_nautical">Pace (min/NM)</string>
<string name="description_pace_metric">Pace (min/km)</string>
<string name="description_recorded_time">Recorded: %1$s</string>
<string name="description_speed_ms">Speed (m/s)</string>
@@ -195,6 +196,7 @@ limitations under the License.
<string name="description_sensor_power">Power (W)</string>
<string name="description_default_speed_or_pace">By activity type</string>
<string name="description_speed_imperial">Speed (mi/hr)</string>
<string name="description_speed_nautical">Speed (knots)</string>
<string name="description_speed_metric">Speed (km/hr)</string>
<string name="description_time">Time</string>
<string name="description_total_distance">Total distance: %1$.2f km (%2$.1f mi)</string>
@@ -435,6 +437,7 @@ limitations under the License.
<string name="settings_stats_rate_title">Preferred rate</string>
<string name="settings_stats_units_imperial">Imperial (mi, ft)</string>
<string name="settings_stats_units_metric">Metric (km, m)</string>
<string name="settings_stats_units_nautical">Nautical (NM, ft)</string>
<string name="settings_stats_units_title">Preferred units</string>
<!-- Settings public api -->
@@ -513,8 +516,11 @@ limitations under the License.
<string name="unit_meter">m</string>
<string name="unit_mile">mi</string>
<string name="unit_mile_per_hour">mph</string>
<string name="unit_nautical_mile">NM</string>
<string name="unit_knots">kn</string>
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/mi</string>
<string name="unit_minute_per_nautical_mile">min/NM</string>
<!-- Location Coordinates -->
<string name="location_coordinate">%1$s°</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
@@ -529,8 +535,11 @@ limitations under the License.
<string name="value_integer_meter_poor_gps">%1$d m (poor GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (recommended)</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
<string name="value_integer_knots">%1$d knots</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_knots">%1$.1f knots</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommended)</string>
<string name="value_float_knots_recommended">%1$.1f knots (recommended)</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</string>
<string name="value_none">None</string>
@@ -557,8 +566,13 @@ limitations under the License.
<item quantity="one">1 mile per hour</item>
<item quantity="other">%1$.1f miles per hour</item>
</plurals>
<plurals name="voiceSpeedMKnots">
<item quantity="one">1 knot</item>
<item quantity="other">%1$.1f knots</item>
</plurals>
<string name="voice_per_kilometer">per kilometer</string>
<string name="voice_per_mile">per mile</string>
<string name="voice_per_nautical_mile">per nautical mile</string>
<string name="lap_time">Lap time</string>
<string name="pace">Pace</string>
<string name="lap_speed">Lap speed</string>
@@ -572,6 +586,10 @@ limitations under the License.
<item quantity="one">1 mile</item>
<item quantity="other">%1$.2f miles</item>
</plurals>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="one">1 nautical mile</item>
<item quantity="other">%1$.2f nautical miles</item>
</plurals>
<string name="average_heart_rate">Average heart rate</string>
<string name="current_heart_rate">Current heart rate</string>
<!-- Waypoint Type -->