Add nautical miles (with feet for altitude).

Fixes #1280.
This commit is contained in:
Dennis Guse
2022-06-13 22:21:10 +02:00
parent 1c3a2bd2e4
commit a283464331
15 changed files with 142 additions and 39 deletions
@@ -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("0.05 NM", 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("knots", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
@@ -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");
}
@@ -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");
}
@@ -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
@@ -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");
}
@@ -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;
+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 -->