Implement elevation gain via barometer.

This commit is contained in:
Dennis Guse
2020-03-30 20:03:04 +02:00
parent 01ee1a1e49
commit 8a14bc46f6
15 changed files with 480 additions and 36 deletions
@@ -32,6 +32,7 @@ public class TrackPoint {
private Float heartRate_bpm = null;
private Float cyclingCadence_rpm = null;
private Float power = null;
private float elevationGain = Float.NaN;
public TrackPoint() {
this.location = new Location("");
@@ -92,6 +93,18 @@ public class TrackPoint {
return location;
}
public boolean hasElevationGain() {
return !Float.isNaN(elevationGain);
}
public float getElevationGain() {
return elevationGain;
}
public void setElevationGain(float elevationGain) {
this.elevationGain = elevationGain;
}
public double getLatitude() {
return location.getLatitude();
}
@@ -87,6 +87,10 @@ public class StatisticsRecordedFragment extends Fragment {
private TextView speedMovingValue;
private TextView speedMovingUnit;
private TextView elevationCurrentGroup;
private TextView elevationTotalGainValue;
private TextView elevationTotalGainUnit;
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
Bundle bundle = new Bundle();
bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -130,6 +134,11 @@ public class StatisticsRecordedFragment extends Fragment {
speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label);
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
elevationCurrentGroup = view.findViewById(R.id.stats_elevation_current_group);
elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value);
elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit);
}
@Override
@@ -187,6 +196,9 @@ public class StatisticsRecordedFragment extends Fragment {
speedMovingLabel = null;
speedMovingValue = null;
speedMovingUnit = null;
elevationTotalGainValue = null;
elevationTotalGainUnit = null;
}
public void loadStatistics() {
@@ -264,5 +276,14 @@ public class StatisticsRecordedFragment extends Fragment {
speedMovingValue.setText(parts.first);
speedMovingUnit.setText(parts.second);
}
// Set elevation gain
{
double elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : 0f;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
elevationTotalGainValue.setText(parts.first);
elevationTotalGainUnit.setText(parts.second);
}
}
}
@@ -132,12 +132,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private TextView speedMovingLabel;
private TextView speedMovingValue;
private TextView speedMovingUnit;
private Group elevationGroup;
private TextView speedLabel;
private TextView speedValue;
private TextView speedUnit;
private TextView elevationValue;
private TextView elevationUnit;
private Group elevationCurrentGroup;
private TextView elevationCurrentValue;
private TextView elevationCurrentUnit;
private TextView elevationTotalGainValue;
private TextView elevationTotalGainUnit;
private Group coordinateGroup;
private TextView latitudeValue;
private TextView longitudeValue;
@@ -178,14 +180,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
elevationGroup = view.findViewById(R.id.stats_elevation_current_group);
speedLabel = view.findViewById(R.id.stats_speed_label);
speedValue = view.findViewById(R.id.stats_speed_value);
speedUnit = view.findViewById(R.id.stats_speed_unit);
elevationValue = view.findViewById(R.id.stats_elevation_current_value);
elevationUnit = view.findViewById(R.id.stats_elevation_current_unit);
elevationCurrentGroup = view.findViewById(R.id.stats_elevation_current_group);
elevationCurrentValue = view.findViewById(R.id.stats_elevation_current_value);
elevationCurrentUnit = view.findViewById(R.id.stats_elevation_current_unit);
elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value);
elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit);
coordinateGroup = view.findViewById(R.id.stats_coordinate_group);
@@ -276,14 +280,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
speedMovingValue = null;
speedMovingUnit = null;
elevationGroup = null;
speedLabel = null;
speedValue = null;
speedUnit = null;
elevationValue = null;
elevationUnit = null;
elevationCurrentGroup = null;
elevationCurrentValue = null;
elevationCurrentUnit = null;
elevationTotalGainValue = null;
elevationTotalGainUnit = null;
coordinateGroup = null;
@@ -406,20 +412,25 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
/**
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
* Also sets elevation gain.
*/
private void updateSensorDataUI() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
SensorDataSet sensorDataSet = null;
Float elevationGain_m = null;
if (trackRecordingService == null) {
Log.d(TAG, "Cannot get the track recording service.");
} else {
sensorDataSet = trackRecordingService.getSensorData();
elevationGain_m = trackRecordingService.getElevationGain_m();
}
setHeartRateSensorData(sensorDataSet);
setCadenceSensorData(sensorDataSet);
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
setElevationGainFromSensor(elevationGain_m);
}
private void setHeartRateSensorData(SensorDataSet sensorDataSet) {
@@ -468,6 +479,33 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
cadenceValueView.setText(sensorValue);
}
// Set elevation gain
public void setTotalElevationGain(Float elevationGain_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
if (elevationGain_m == null) {
elevationGain_m = 0f;
}
if (lastTrackStatistics != null) {
elevationGain_m += (float) lastTrackStatistics.getTotalElevationGain();
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
elevationTotalGainValue.setText(parts.first);
elevationTotalGainUnit.setText(parts.second);
}
public void setElevationGainFromSensor(Float elevationGain_m) {
float totalElevationGain = 0;
if (elevationGain_m != null) {
totalElevationGain = elevationGain_m;
}
if (lastTrackStatistics != null) {
totalElevationGain += (float) lastTrackStatistics.getTotalElevationGain();
}
setTotalElevationGain(totalElevationGain);
}
/**
* If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too.
*/
@@ -475,7 +513,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (cadenceGroup.getVisibility() != View.VISIBLE && heartRateGroup.getVisibility() != View.VISIBLE) {
sensorHorizontalLine.setVisibility(View.GONE);
}
}
private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
@@ -555,10 +592,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
speedMovingUnit.setText(parts.second);
}
// Make elevation visible?
// Set elevation (gain)
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
elevationCurrentGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
}
}
@@ -594,14 +632,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set elevation
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
elevationCurrentGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
// Current elevation
double altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? lastTrackPoint.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
elevationValue.setText(parts.first);
elevationUnit.setText(parts.second);
elevationCurrentValue.setText(parts.first);
elevationCurrentUnit.setText(parts.second);
}
// Set coordinate
@@ -49,6 +49,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.ElevationSumManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -115,6 +116,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// The following variables are set when recording:
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private ElevationSumManager elevationSumManager;
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
@@ -174,6 +176,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager = null;
}
if (elevationSumManager != null) {
elevationSumManager.stop(this);
elevationSumManager = null;
}
// Reverse order from onCreate
showNotification(false); //TODO Why?
@@ -371,6 +378,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// Update instance variables
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
elevationSumManager = new ElevationSumManager();
elevationSumManager.start(this);
lastTrackPoint = null;
isIdle = false;
@@ -456,6 +467,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager.stop();
remoteSensorManager = null;
}
if (elevationSumManager != null) {
elevationSumManager.stop(this);
elevationSumManager = null;
}
lastTrackPoint = null;
handlerServer.stop(this);
@@ -533,6 +549,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (elevationSumManager != null) {
trackPoint.setElevationGain(elevationSumManager.getElevationGain_m());
}
//Storing trackPoint
// Always insert the first segment location
@@ -556,15 +576,19 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
insertTrackPoint(track, TrackPoint.createPause());
insertTrackPoint(track, trackPoint);
isIdle = false;
elevationSumManager.reset();
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
elevationSumManager.reset();
isIdle = false;
lastTrackPoint = trackPoint;
@@ -573,7 +597,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
elevationSumManager.reset();
isIdle = true;
lastTrackPoint = trackPoint;
@@ -582,7 +609,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
elevationSumManager.reset();
isIdle = false;
lastTrackPoint = trackPoint;
@@ -672,6 +702,17 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
/**
* Returns the relative elevation gain (since last trackpoint).
*/
Float getElevationGain_m() {
if (elevationSumManager == null || !elevationSumManager.isConnected()) {
return null;
}
return elevationSumManager.getElevationGain_m();
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
@@ -99,6 +99,11 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
return trackRecordingService.getSensorDataSet();
}
@Override
public Float getElevationGain_m() {
return trackRecordingService.getElevationGain_m();
}
@VisibleForTesting
@Override
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
@@ -65,12 +65,19 @@ public interface TrackRecordingServiceInterface {
Marker.Id insertMarker(String name, String category, String description, String photoUrl);
/**
* Gets the current sensor data. Returns null if there is no data.
* Gets the current sensor data.
*
* @return SensorDataSet object.
* @return SensorDataSet object or null.
*/
SensorDataSet getSensorData();
/**
* Gets the current elevation gain.
*
* @return elevation gain in m or null.
*/
Float getElevationGain_m();
@VisibleForTesting
void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager);
@@ -0,0 +1,102 @@
package de.dennisguse.opentracks.services.sensors;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.util.PressureSensorUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Estimates the elevation gain and elevation loss using the device's pressure sensor (i.e., barometer).
*/
public class ElevationSumManager implements SensorEventListener {
private static final String TAG = ElevationSumManager.class.getSimpleName();
private static final int SAMPLING_RATE = 3 * (int) UnitConversions.ONE_SECOND_US;
private boolean isConnected = false;
private float lastUsedPressureValue_hPa;
private float elevationGain_m;
private float elevationLoss_m;
public void start(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
}
isConnected = sensorManager.registerListener(this, pressureSensor, SAMPLING_RATE);
lastUsedPressureValue_hPa = Float.NaN;
reset();
}
public void stop(Context context) {
Log.d(TAG, "Stop");
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(this);
isConnected = false;
reset();
}
public boolean isConnected() {
return isConnected;
}
public float getElevationGain_m() {
return elevationGain_m;
}
public float getElevationLoss_m() {
return elevationLoss_m;
}
public void reset() {
Log.d(TAG, "Reset");
elevationGain_m = 0;
elevationLoss_m = 0;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
@Override
public void onSensorChanged(SensorEvent event) {
onSensorValueChanged(event.values[0]);
}
@VisibleForTesting
void onSensorValueChanged(float value_hPa) {
if (Float.isNaN(lastUsedPressureValue_hPa)) {
lastUsedPressureValue_hPa = value_hPa;
return;
}
PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, value_hPa);
if (elevationChange != null) {
if (elevationChange.getElevationChange_m() > 0) {
elevationGain_m += elevationChange.getElevationChange_m();
} else {
elevationLoss_m += elevationChange.getElevationChange_m();
}
lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa();
}
Log.v(TAG, "elevation gain: " + elevationGain_m);
}
}
@@ -228,7 +228,7 @@ public class TrackStatistics {
}
/**
* Gets the total elevation gain in meters. This is calculated as the sum of all positive differences in the smoothed elevation.
* Gets the total elevation gain in meters.
*/
public double getTotalElevationGain() {
return totalElevationGain_m;
@@ -112,7 +112,7 @@ public class TrackStatisticsUpdater {
* Adds a trackPoint.
* TODO: This assume trackPoint has a valid time.
*
* @param trackPoint the trackPoint
* @param trackPoint the trackPoint
* @param minRecordingDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
@@ -134,8 +134,16 @@ public class TrackStatisticsUpdater {
return;
}
//TODO Use Barometer to compute elevation gain.
double elevationDifference = trackPoint.hasAltitude() ? updateElevation(trackPoint.getAltitude()) : 0.0;
//Update absolute (GPS-based) elevation
if (trackPoint.hasAltitude()) {
updateAbsoluteElevation(trackPoint.getAltitude());
}
//Get elevation gain
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
Log.d(TAG, "elevation gain: " + trackPoint.getElevationGain());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
lastTrackPoint = trackPoint;
@@ -207,23 +215,21 @@ public class TrackStatisticsUpdater {
}
/**
* Updates an elevation reading. Returns the difference.
* Updates an elevation reading.
*
* @param elevation the elevation
* @return the difference
*/
@VisibleForTesting
private double updateElevation(double elevation) {
private double updateAbsoluteElevation(double elevation) {
// Update elevation using the smoothed average
double oldAverage = elevationBuffer_m.getAverage();
elevationBuffer_m.setNext(elevation);
double newAverage = elevationBuffer_m.getAverage();
currentSegment.updateElevationExtremities(newAverage);
double difference = newAverage - oldAverage;
if (difference > 0) {
currentSegment.addTotalElevationGain(difference);
}
return difference;
return newAverage - oldAverage;
}
private TrackStatistics init(long time) {
@@ -0,0 +1,49 @@
package de.dennisguse.opentracks.util;
import android.hardware.SensorManager;
public class PressureSensorUtils {
//Everything above is considered a meaningful change in elevation.
private static float ELEVATION_CHANGE_DIFF_M = 5.0f;
private PressureSensorUtils() {
}
public static class ElevationChange {
private float currentSensorValue_hPa;
private float elevationChange_m;
public ElevationChange(float currentSensorValue_hPa, float elevationChange_m) {
this.currentSensorValue_hPa = currentSensorValue_hPa;
this.elevationChange_m = elevationChange_m;
}
public float getCurrentSensorValue_hPa() {
return currentSensorValue_hPa;
}
public float getElevationChange_m() {
return elevationChange_m;
}
}
/**
* Computes the elevation gain and elevation loss.
*
* @return null if no meaningful elevation change occurred.
*/
public static ElevationChange computeChanges_m(float lastSensorValue_hPa, float currentSensorValue_hPa) {
float lastSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_hPa);
float currentSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, currentSensorValue_hPa);
float elevationChange_m = currentSensorValue_m - lastSensorValue_m;
if (Math.abs(elevationChange_m) < ELEVATION_CHANGE_DIFF_M) {
return null;
}
return new ElevationChange(currentSensorValue_hPa, elevationChange_m);
}
}
@@ -26,7 +26,8 @@ public class UnitConversions {
public static final long S_TO_MS = 1000;
// Time
// 1 second in milliseconds
// TODO Use int instead of long.
public static final long ONE_SECOND_US = UnitConversions.S_TO_MS * 1000;
public static final long ONE_SECOND_MS = UnitConversions.S_TO_MS;
public static final long ONE_MINUTE_MS = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
public static final long ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
@@ -209,6 +209,48 @@
app:layout_constraintRight_toLeftOf="@+id/guideline3"
tools:text="km/h" />
<!-- Elevation gain -->
<View
android:id="@+id/stats_elevation_horizontal_line"
style="@style/StatsHorizontalLine"
android:layout_marginBottom="8dp"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_moving_speed_value" />
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_elevation_gain_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_elevation_current_label,stats_elevation_current_value,stats_elevation_current_unit,stats_elevation_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_label"
style="@style/StatsSmallLabel"
android:text="@string/stats_gain"
app:layout_constrainedWidth="true"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/stats_elevation_gain_unit"
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_gain_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_gain_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@+id/stats_elevation_gain_value"
app:layout_constraintLeft_toRightOf="@+id/stats_elevation_gain_value"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
tools:text="m" />
<!-- Horizontal line -->
<View
android:id="@+id/stats_speed_horizontal_end_line"
+37 -3
View File
@@ -258,8 +258,8 @@
style="@style/StatsSmallLabel"
android:text="@string/stats_elevation"
app:layout_constrainedWidth="true"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
<TextView
@@ -267,7 +267,7 @@
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/stats_elevation_current_unit"
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_current_label"
tools:text="100" />
@@ -277,6 +277,40 @@
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@+id/stats_elevation_current_value"
app:layout_constraintLeft_toRightOf="@+id/stats_elevation_current_value"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
tools:text="m" />
<!-- Current elevation -->
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_elevation_gain_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_elevation_current_label,stats_elevation_current_value,stats_elevation_current_unit,stats_elevation_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_label"
style="@style/StatsSmallLabel"
android:text="@string/stats_gain"
app:layout_constrainedWidth="true"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/stats_elevation_gain_unit"
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_gain_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_gain_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@+id/stats_elevation_gain_value"
app:layout_constraintLeft_toRightOf="@+id/stats_elevation_gain_value"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
tools:text="m" />