mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-24 19:24:48 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14e63a1a54 | |||
| e559583d23 | |||
| 3b9711d82e | |||
| 114fc8255f | |||
| 26e655d58f | |||
| 815b6a3cf1 | |||
| 3dc8a724d8 | |||
| 70f571cd09 | |||
| 3f5c3a5c08 | |||
| a13465391f | |||
| 97454b5369 | |||
| f6a47c4594 | |||
| fce76230e1 | |||
| fdf4336a9b | |||
| 844c6e9609 | |||
| 784942ffd2 | |||
| edf4ccc941 | |||
| d90258294f | |||
| 01f4c13307 | |||
| 873956426d | |||
| 4128c54960 | |||
| 9978bddf21 | |||
| a9726b9a94 | |||
| 05ba52bab5 | |||
| ca95f40e77 | |||
| 5cb85c4383 | |||
| d0a477b112 | |||
| ec365a18ca | |||
| 0ff735e559 | |||
| 8a35835791 | |||
| e3569dab56 | |||
| 4040e3f45b | |||
| eac7efba64 | |||
| 21a3971a56 | |||
| 4e4a9abf6f | |||
| a9624f72e7 | |||
| bb38d274fb |
@@ -18,4 +18,6 @@ jobs:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
arch: x86_64
|
||||
profile: Nexus 6
|
||||
script: ./gradlew connectedCheck
|
||||
|
||||
|
||||
+4
-4
@@ -51,8 +51,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 3855
|
||||
versionName "v3.15.0"
|
||||
versionCode 3892
|
||||
versionName "v3.15.4"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -87,7 +87,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
@@ -99,5 +99,5 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.7.7'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.15.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- After recording show sensor statistics (e.g., avg HR)
|
||||
|
||||
Bugfix:
|
||||
- Crash on recording start with show latitude/longitude enabled
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.15.2: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Updated translations
|
||||
|
||||
Bugfix:
|
||||
- Crash on GPX export
|
||||
@@ -0,0 +1,8 @@
|
||||
v3.15.3: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Computation avg/max: heart rate, cadence, and power
|
||||
|
||||
Bugfix:
|
||||
- KML/KMZ do not export #start/#end placemarks anymore
|
||||
- While recording: Speed/location was sometimes falsly shown as 0.
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.15.4: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Voice annoucements interval should be in minutes (introduced in v3.15.0)
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TestSensorDataUtil {
|
||||
|
||||
private List<TrackPoint> trackPointList = new ArrayList<>();
|
||||
private List<SensorData> sensorDataList = new ArrayList<>();
|
||||
|
||||
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
|
||||
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
|
||||
TrackPoint tp = new TrackPoint(type);
|
||||
int i = trackPointList.size() + 1;
|
||||
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
|
||||
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
|
||||
tp.setHeartRate_bpm(hr);
|
||||
tp.setCyclingCadence_rpm(cadence);
|
||||
tp.setPower(power);
|
||||
tp.setAccuracy(1f);
|
||||
tp.setAltitude(1f);
|
||||
tp.setTime(time);
|
||||
tp.setSpeed(5f + (i / 10f));
|
||||
tp.setElevationGain(3f);
|
||||
tp.setElevationLoss(3f);
|
||||
trackPointList.add(tp);
|
||||
}
|
||||
|
||||
public List<TrackPoint> getTrackPointList() {
|
||||
return this.trackPointList;
|
||||
}
|
||||
|
||||
public SensorDataStats computeStats() {
|
||||
if (sensorDataList == null || sensorDataList.size() <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SensorDataStats stats = new SensorDataStats();
|
||||
long timeElapsed;
|
||||
long movingTime = 0;
|
||||
SensorData dataPrev = sensorDataList.get(0);
|
||||
stats.maxHr = dataPrev.hr;
|
||||
stats.maxCadence = dataPrev.cadence;
|
||||
SensorData dataCurrent;
|
||||
for (int i = 1; i < sensorDataList.size(); i++) {
|
||||
dataCurrent = sensorDataList.get(i);
|
||||
if (dataPrev.type != TrackPoint.Type.SEGMENT_START_MANUAL) {
|
||||
timeElapsed = dataCurrent.type != TrackPoint.Type.SEGMENT_START_MANUAL ? dataCurrent.time.getEpochSecond() - dataPrev.time.getEpochSecond() : 0;
|
||||
stats.avgHr += (dataPrev.hr * timeElapsed);
|
||||
stats.maxHr = dataPrev.hr > stats.maxHr ? dataPrev.hr : stats.maxHr;
|
||||
stats.avgCadence += (dataPrev.cadence * timeElapsed);
|
||||
stats.maxCadence = dataPrev.cadence > stats.maxCadence ? dataPrev.cadence : stats.maxCadence;
|
||||
stats.avgPower += (dataPrev.power * timeElapsed);
|
||||
|
||||
movingTime += timeElapsed;
|
||||
}
|
||||
dataPrev = dataCurrent;
|
||||
}
|
||||
|
||||
stats.avgHr /= movingTime;
|
||||
stats.avgCadence /= movingTime;
|
||||
stats.avgPower /= movingTime;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private static class SensorData {
|
||||
Instant time;
|
||||
float hr;
|
||||
float cadence;
|
||||
float power;
|
||||
TrackPoint.Type type;
|
||||
|
||||
public SensorData(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
|
||||
this.time = time;
|
||||
this.hr = hr == null ? 0 : hr;
|
||||
this.cadence = cadence == null ? 0 : cadence;
|
||||
this.power = power == null ? 0 : power;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorDataStats {
|
||||
public float avgHr = 0f;
|
||||
public float maxHr = 0f;
|
||||
public float avgCadence = 0f;
|
||||
public float maxCadence = 0f;
|
||||
public float avgPower = 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TrackPointTest {
|
||||
|
||||
@Test
|
||||
public void isRecent_true() {
|
||||
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
tp.setTime(Instant.now());
|
||||
|
||||
assertTrue(tp.isRecent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isRecent_false() {
|
||||
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
tp.setTime(Instant.now().minus(2, ChronoUnit.MINUTES));
|
||||
|
||||
assertFalse(tp.isRecent());
|
||||
}
|
||||
}
|
||||
+337
@@ -35,18 +35,22 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
@@ -927,4 +931,337 @@ public class CustomContentProviderUtilsTest {
|
||||
assertEquals("12", ContentProviderUtils.formatIdListForUri(new Track.Id(12)));
|
||||
assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new Track.Id(42), new Track.Id(43), new Track.Id(44)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_noSensorData() {
|
||||
// given
|
||||
List<TrackPoint> trackPointList = new ArrayList<>();
|
||||
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
|
||||
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
|
||||
trackPoint.setPower(null);
|
||||
trackPoint.setCyclingCadence_rpm(null);
|
||||
trackPoint.setHeartRate_bpm(null);
|
||||
trackPointList.add(trackPoint);
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, trackPointList);
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_needAtLeastTwoTrackPointsFalse() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 90 300 0
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_needAtLeastTwoTrackPointsTrue() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 90 300 -1
|
||||
* 1 140 90 300 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, 90f, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertTrue(sensorStatistics.hasHeartRate());
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertTrue(sensorStatistics.hasCadence());
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertTrue(sensorStatistics.hasPower());
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyHr() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 NULL NULL -1
|
||||
* 1 140 NULL NULL 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, null, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, null, null, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertTrue(sensorStatistics.hasHeartRate());
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyCadence() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 NULL 90 NULL -1
|
||||
* 1 NULL 90 NULL 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, null, 90f, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, 90f, null, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertTrue(sensorStatistics.hasCadence());
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyPower() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 NULL NULL 300 -1
|
||||
* 1 NULL NULL 300 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, null, null, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, null, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertTrue(sensorStatistics.hasPower());
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 7 160 90 275 0
|
||||
* 4 155 85 280 0
|
||||
* 1 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(10, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(14, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(15, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withManualResume() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 3 174 88 400 0
|
||||
* 20 127 54 175 -2
|
||||
* 3 160 90 275 0
|
||||
* 7 155 85 280 0
|
||||
* 3 150 90 267 0
|
||||
* 3 170 90 240 0
|
||||
* 2 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_MANUAL);
|
||||
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withStartAutomatic() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 3 174 88 400 0
|
||||
* 20 127 54 175 -1
|
||||
* 3 160 90 275 0
|
||||
* 7 155 85 280 0
|
||||
* 3 150 90 267 0
|
||||
* 3 170 90 240 0
|
||||
* 2 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
|
||||
// given
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < totalPoints; i++) {
|
||||
int randomNum = withStartSegments ? random.nextInt(50) - 2 : 0;
|
||||
TrackPoint.Type type = randomNum >= 0 ? TrackPoint.Type.TRACKPOINT : TrackPoint.Type.getById(randomNum);
|
||||
float randomHr = random.nextFloat() * (200f - 90f) + 90f;
|
||||
float randomCadence = random.nextFloat() * (110f - 40f) + 40f;
|
||||
float randomPower = random.nextFloat() * (500f - 100f) + 100f;
|
||||
sensorDataUtil.add(start.plus(i, ChronoUnit.SECONDS), randomHr, randomCadence, randomPower, type);
|
||||
}
|
||||
sensorDataUtil.add(start.plus(totalPoints, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0.01f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0.01f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_veryLongActivity12h() {
|
||||
testGetSensorStats_randomData(43200, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withSeveralRandomStartSegments() {
|
||||
testGetSensorStats_randomData(5000, true);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -24,7 +24,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -37,7 +37,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(33.53, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(33.53, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -50,7 +50,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(0, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(0, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertFalse(current.hasCadence_rpm());
|
||||
assertFalse(current.hasValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,7 +77,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,7 +90,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,7 +103,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous, 2150);
|
||||
|
||||
// then
|
||||
assertEquals(1.20, current.getSpeed_mps(), 0.01);
|
||||
assertEquals(1.20, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,7 +116,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous, 2000);
|
||||
|
||||
// then
|
||||
assertEquals(2, current.getSpeed_mps(), 0.01);
|
||||
assertEquals(2, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -26,7 +26,9 @@ public class AnnouncementUtilsTest {
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void getAnnouncement() {
|
||||
public void getAnnouncement_metric() {
|
||||
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
|
||||
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
@@ -42,7 +44,9 @@ public class AnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval() {
|
||||
public void getAnnouncement_withInterval_metric() {
|
||||
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
|
||||
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
@@ -54,30 +58,12 @@ public class AnnouncementUtilsTest {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
|
||||
int speedId = R.plurals.voiceSpeedKilometersPerHour;
|
||||
double kmPerHour = lastInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH;
|
||||
|
||||
String firstPartMsg = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour";
|
||||
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
|
||||
String msg = firstPartMsg + rateMsg;
|
||||
String expected = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour Lap speed of 51.2 kilometers per hour";
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals(msg, announcement);
|
||||
}
|
||||
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
int count = (int) d;
|
||||
return Math.max(count, 3);
|
||||
}
|
||||
assertEquals(expected, announcement);
|
||||
}
|
||||
}
|
||||
@@ -94,18 +94,12 @@ public class StringUtilsTest {
|
||||
assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#parseTime(String)} with fractional seconds.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTime_fractional() {
|
||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#parseTime(String)} with time zone.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTime_timezone() {
|
||||
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
|
||||
@@ -118,9 +112,6 @@ public class StringUtilsTest {
|
||||
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#parseTime(String)} with fractional seconds and time zone.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTime_fractionalAndTimezone() {
|
||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
|
||||
@@ -84,11 +84,9 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
// Setup UI elements
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
|
||||
viewBinding.markerEditMarkerType.setAdapter(adapter);
|
||||
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
|
||||
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
|
||||
viewBinding.markerEditMarkerType.getText().toString(),
|
||||
viewBinding.markerEditDescription.getText().toString());
|
||||
});
|
||||
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
|
||||
viewBinding.markerEditMarkerType.getText().toString(),
|
||||
viewBinding.markerEditDescription.getText().toString()));
|
||||
|
||||
viewBinding.markerEditCancel.setOnClickListener(v -> {
|
||||
viewModel.onCancel();
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.util.concurrent.Future;
|
||||
*/
|
||||
class ExecutorListViewService<T> {
|
||||
|
||||
private List<T> enqueueObjects = new ArrayList<>();
|
||||
private ExecutorService executorService;
|
||||
private final List<T> enqueueObjects = new ArrayList<>();
|
||||
private final ExecutorService executorService;
|
||||
|
||||
public ExecutorListViewService(int numThreads) {
|
||||
executorService = Executors.newFixedThreadPool(numThreads);
|
||||
@@ -23,7 +23,6 @@ class ExecutorListViewService<T> {
|
||||
|
||||
public void shutdown() {
|
||||
enqueueObjects.clear();
|
||||
enqueueObjects = null;
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
private ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
//TODO Should be Marker.Id
|
||||
private final ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
|
||||
private boolean scroll = false;
|
||||
|
||||
@@ -111,8 +112,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
|
||||
return;
|
||||
}
|
||||
|
||||
long id = cursor.getLong(cursor.getColumnIndex(MarkerColumns._ID));
|
||||
String photoUrl = cursor.getString(cursor.getColumnIndex(MarkerColumns.PHOTOURL));
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
|
||||
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
|
||||
|
||||
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
|
||||
if (hasPhoto) {
|
||||
|
||||
@@ -12,7 +12,7 @@ public class ScrollVisibleViews implements AbsListView.OnScrollListener {
|
||||
private int from = -1;
|
||||
private int to = -1;
|
||||
|
||||
private VisibleViewsListener visibleViewsListener;
|
||||
private final VisibleViewsListener visibleViewsListener;
|
||||
|
||||
public ScrollVisibleViews(@NonNull VisibleViewsListener visibleViewsListener) {
|
||||
this.visibleViewsListener = visibleViewsListener;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -13,19 +12,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
public static final int HEART_RATE_TYPE = 0;
|
||||
public static final int CADENCE_TYPE = 1;
|
||||
public static final int POWER_TYPE = 2;
|
||||
|
||||
private List<Pair<Integer, SensorData>> sensorDataList;
|
||||
private List<SensorDataModel> sensorDataList;
|
||||
private final Context context;
|
||||
|
||||
public SensorsAdapter(Context context) {
|
||||
@@ -42,9 +33,8 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
|
||||
int type = sensorDataList.get(position).first;
|
||||
SensorData sensorData = sensorDataList.get(position).second;
|
||||
viewHolder.setData(sensorData, type);
|
||||
SensorDataModel sensorDataModel = sensorDataList.get(position);
|
||||
viewHolder.setData(sensorDataModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,12 +46,7 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return sensorDataList.get(position).first;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
|
||||
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
|
||||
if (sensorDataList == data) {
|
||||
return null;
|
||||
}
|
||||
@@ -89,70 +74,18 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
unit = itemView.findViewById(R.id.stats_sensor_unit);
|
||||
}
|
||||
|
||||
public void setData(SensorData sensorData, int type) {
|
||||
switch (type) {
|
||||
case HEART_RATE_TYPE:
|
||||
setHeartRateSensorData((SensorDataHeartRate) sensorData);
|
||||
break;
|
||||
case CADENCE_TYPE:
|
||||
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
|
||||
break;
|
||||
case POWER_TYPE:
|
||||
setPowerSensorData((SensorDataCyclingPower) sensorData);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown sensor type");
|
||||
}
|
||||
}
|
||||
public void setData(SensorDataModel sensorDataModel) {
|
||||
String sensorName = sensorDataModel.getSensorName();
|
||||
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
|
||||
|
||||
private void setHeartRateSensorData(SensorDataHeartRate data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
||||
}
|
||||
this.label.setText(context.getString(sensorDataModel.getLabelId()));
|
||||
if (sensorName == null) {
|
||||
this.sensorValue.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.sensorValue.setText(sensorName);
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_beats_per_minute);
|
||||
}
|
||||
|
||||
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasCadence_rpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_cadence));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
|
||||
}
|
||||
|
||||
private void setPowerSensorData(SensorDataCyclingPower data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasPower_w() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_power));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_power);
|
||||
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,10 +365,11 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = null;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
trackPoint = trackPointIterator.next();
|
||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||
|
||||
// Stop if past the last wanted point
|
||||
@@ -398,18 +399,23 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
}
|
||||
|
||||
localNumLoadedTrackPoints++;
|
||||
localLastSeenTrackPointIdId = trackPointId;
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint != null) {
|
||||
localLastSeenTrackPointIdId = trackPoint.getId();
|
||||
}
|
||||
|
||||
if (updateSamplingState) {
|
||||
numLoadedPoints = localNumLoadedTrackPoints;
|
||||
firstSeenTrackPointId = localFirstSeenTrackPointId;
|
||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
listener.onNewTrackPointsDone();
|
||||
if (trackPoint != null) {
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
listener.onNewTrackPointsDone(trackPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -32,6 +34,7 @@ public interface TrackDataListener {
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
//TODO Could be @NonNull
|
||||
void onTrackUpdated(Track track);
|
||||
|
||||
/**
|
||||
@@ -44,20 +47,20 @@ public interface TrackDataListener {
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
void onSampledInTrackPoint(TrackPoint trackPoint);
|
||||
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
|
||||
|
||||
/**
|
||||
* Called when a sampled out track point is read.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
void onSampledOutTrackPoint(TrackPoint trackPoint);
|
||||
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
|
||||
|
||||
/**
|
||||
* Called when finish sending new track points.
|
||||
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
|
||||
*/
|
||||
void onNewTrackPointsDone();
|
||||
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
|
||||
|
||||
/**
|
||||
* Called to clear previously sent markers.
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.os.Parcel;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -42,6 +43,11 @@ import java.util.Objects;
|
||||
*/
|
||||
public class TrackPoint {
|
||||
|
||||
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
private TrackPoint.Id id;
|
||||
|
||||
private Instant time;
|
||||
@@ -174,7 +180,23 @@ public class TrackPoint {
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null || longitude != null;
|
||||
return latitude != null && longitude != null;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -222,22 +244,6 @@ public class TrackPoint {
|
||||
this.elevationLoss = elevationLoss;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
@@ -246,6 +252,12 @@ public class TrackPoint {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public boolean isRecent() {
|
||||
return Instant.now()
|
||||
.isBefore(time.plus(MAX_LOCATION_AGE));
|
||||
}
|
||||
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude_m != null;
|
||||
}
|
||||
@@ -270,6 +282,10 @@ public class TrackPoint {
|
||||
this.speed_mps = speed;
|
||||
}
|
||||
|
||||
public boolean isMoving() {
|
||||
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
@@ -298,6 +314,10 @@ public class TrackPoint {
|
||||
return getLocation().distanceTo(dest.getLocation());
|
||||
}
|
||||
|
||||
public boolean fulfillsAccuracy(int poorAccuracy) {
|
||||
return hasAccuracy() && accuracy < poorAccuracy;
|
||||
}
|
||||
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
return getLocation().bearingTo(dest.getLocation());
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
String ELEVATION_GAIN = "elevation_gain";
|
||||
String ELEVATION_LOSS = "elevation_loss";
|
||||
|
||||
// Alias for sensor statistics
|
||||
String ALIAS_AVG_HR = "avg_hr";
|
||||
String ALIAS_MAX_HR = "max_hr";
|
||||
String ALIAS_AVG_CADENCE = "avg_cadence";
|
||||
String ALIAS_MAX_CADENCE = "max_cadence";
|
||||
String ALIAS_AVG_POWER = "avg_power";
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface TracksColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "tracks";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -44,6 +45,7 @@ import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
@@ -577,24 +579,6 @@ public class ContentProviderUtils {
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first location id for a track.
|
||||
* Returns null if it doesn't exist.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
@Deprecated
|
||||
public Track.Id getFirstTrackPointId(Track.Id trackId) {
|
||||
String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
|
||||
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return new Track.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last location id for a track.
|
||||
* Returns -1L if it doesn't exist.
|
||||
@@ -835,4 +819,21 @@ public class ContentProviderUtils {
|
||||
public static String[] parseTrackIdsFromUri(Uri url) {
|
||||
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
|
||||
}
|
||||
|
||||
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
|
||||
SensorStatistics sensorStatistics = null;
|
||||
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
sensorStatistics = new SensorStatistics(
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER)) : null
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return sensorStatistics;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
|
||||
@@ -56,6 +57,38 @@ public class CustomContentProvider extends ContentProvider {
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
/**
|
||||
* The string representing the query that compute sensor stats from trackpoints table.
|
||||
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
|
||||
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
|
||||
*/
|
||||
private final String SENSOR_STATS_QUERY =
|
||||
"WITH time_select as " +
|
||||
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
|
||||
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
|
||||
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
|
||||
|
||||
"SELECT " +
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
|
||||
|
||||
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
|
||||
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
|
||||
|
||||
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
|
||||
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
|
||||
|
||||
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
|
||||
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
|
||||
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
|
||||
|
||||
public CustomContentProvider() {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
|
||||
@@ -63,6 +96,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
|
||||
@@ -223,6 +257,10 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
break;
|
||||
case TRACKS_SENSOR_STATS:
|
||||
long trackId = ContentUris.parseId(url);
|
||||
Cursor cursor = db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
|
||||
return cursor;
|
||||
case MARKERS:
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
|
||||
@@ -363,6 +401,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
TRACKPOINTS_BY_TRACKID,
|
||||
TRACKS,
|
||||
TRACKS_BY_ID,
|
||||
TRACKS_SENSOR_STATS,
|
||||
MARKERS,
|
||||
MARKERS_BY_ID,
|
||||
MARKERS_BY_TRACKID
|
||||
|
||||
@@ -8,12 +8,14 @@ import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
|
||||
public class SensorData {
|
||||
public abstract class SensorData<T> {
|
||||
|
||||
protected T value;
|
||||
|
||||
private final String sensorAddress;
|
||||
private final String sensorName;
|
||||
|
||||
private final Instant timestamp_ms;
|
||||
private final Instant time;
|
||||
|
||||
SensorData(String sensorAddress) {
|
||||
this(sensorAddress, null);
|
||||
@@ -24,10 +26,10 @@ public class SensorData {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SensorData(String sensorAddress, String sensorName, Instant timestamp_ms) {
|
||||
SensorData(String sensorAddress, String sensorName, Instant time) {
|
||||
this.sensorAddress = sensorAddress;
|
||||
this.sensorName = sensorName;
|
||||
this.timestamp_ms = timestamp_ms;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -44,7 +46,19 @@ public class SensorData {
|
||||
return sensorName != null ? sensorName : sensorAddress;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*/
|
||||
public boolean isRecent() {
|
||||
return Instant.now().isBefore(timestamp_ms.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
|
||||
return Instant.now()
|
||||
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -20,11 +21,10 @@ public final class SensorDataCycling {
|
||||
private SensorDataCycling() {
|
||||
}
|
||||
|
||||
public static class Cadence extends SensorData {
|
||||
public static class Cadence extends SensorData<Float> {
|
||||
|
||||
private final Long crankRevolutionsCount; // UINT32
|
||||
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float cadence_rpm;
|
||||
|
||||
public Cadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
@@ -57,24 +57,16 @@ public final class SensorDataCycling {
|
||||
return crankRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasCadence_rpm() {
|
||||
return cadence_rpm != null;
|
||||
}
|
||||
|
||||
public float getCadence_rpm() {
|
||||
return cadence_rpm;
|
||||
}
|
||||
|
||||
public void compute(Cadence previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
cadence_rpm = null;
|
||||
value = null;
|
||||
} else {
|
||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
float cadence_ms = crankDiff / timeDiff_ms;
|
||||
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
|
||||
value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +74,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
return "cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,11 +90,10 @@ public final class SensorDataCycling {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Speed extends SensorData {
|
||||
public static class Speed extends SensorData<Float> {
|
||||
|
||||
private final Integer wheelRevolutionsCount; // UINT16
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float speed_mps;
|
||||
|
||||
public Speed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
@@ -128,24 +119,16 @@ public final class SensorDataCycling {
|
||||
return wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasSpeed_mps() {
|
||||
return speed_mps != null;
|
||||
}
|
||||
|
||||
public float getSpeed_mps() {
|
||||
return speed_mps;
|
||||
}
|
||||
|
||||
public void compute(Speed previous, int wheel_circumference_mm) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
speed_mps = null;
|
||||
value = null;
|
||||
} else {
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
|
||||
speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
|
||||
value = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +136,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
return "speed=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,23 +152,19 @@ public final class SensorDataCycling {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CadenceAndSpeed extends SensorData {
|
||||
|
||||
private final Cadence cadence;
|
||||
private final Speed speed;
|
||||
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, Speed>> {
|
||||
|
||||
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.cadence = cadence;
|
||||
this.speed = speed;
|
||||
this.value = new Pair<>(cadence, speed);
|
||||
}
|
||||
|
||||
public Cadence getCadence() {
|
||||
return cadence;
|
||||
return this.value != null ? this.value.first : null;
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
return this.value != null ? this.value.second : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataCyclingPower extends SensorData {
|
||||
|
||||
private final Float power_w;
|
||||
public class SensorDataCyclingPower extends SensorData<Float> {
|
||||
|
||||
public SensorDataCyclingPower(String address) {
|
||||
super(address);
|
||||
this.power_w = null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower(String name, String address, float power_w) {
|
||||
super(name, address);
|
||||
this.power_w = power_w;
|
||||
}
|
||||
|
||||
public boolean hasPower_w() {
|
||||
return power_w != null;
|
||||
}
|
||||
|
||||
public float getPower_w() {
|
||||
return power_w;
|
||||
this.value = power_w;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "power=" + power_w;
|
||||
return "power=" + value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataHeartRate extends SensorData {
|
||||
|
||||
private final Float heartRate_bpm;
|
||||
public class SensorDataHeartRate extends SensorData<Float> {
|
||||
|
||||
public SensorDataHeartRate(String address) {
|
||||
super(address);
|
||||
heartRate_bpm = null;
|
||||
}
|
||||
|
||||
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
||||
super(name, address);
|
||||
this.heartRate_bpm = heartRate_bpm;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate_bpm() {
|
||||
return heartRate_bpm != null;
|
||||
}
|
||||
|
||||
public Float getHeartRate_bpm() {
|
||||
return heartRate_bpm;
|
||||
this.value = heartRate_bpm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "heart=" + heartRate_bpm;
|
||||
return "heart=" + value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ public final class SensorDataSet {
|
||||
return cyclingPower;
|
||||
}
|
||||
|
||||
public void set(SensorData data) {
|
||||
public void set(SensorData<?> data) {
|
||||
set(data, data);
|
||||
}
|
||||
|
||||
public void remove(SensorData type) {
|
||||
public void remove(SensorData<?> type) {
|
||||
set(type, null);
|
||||
}
|
||||
|
||||
@@ -50,19 +50,19 @@ public final class SensorDataSet {
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
if (heartRate != null) {
|
||||
trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm());
|
||||
trackPoint.setHeartRate_bpm(heartRate.getValue());
|
||||
}
|
||||
|
||||
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
|
||||
trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm());
|
||||
if (cyclingCadence != null && cyclingCadence.hasValue()) {
|
||||
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue());
|
||||
}
|
||||
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
|
||||
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasValue()) {
|
||||
trackPoint.setSpeed(cyclingSpeed.getValue());
|
||||
}
|
||||
|
||||
if (cyclingPower != null && cyclingPower.hasPower_w()) {
|
||||
trackPoint.setPower(cyclingPower.getPower_w());
|
||||
if (cyclingPower != null && cyclingPower.hasValue()) {
|
||||
trackPoint.setPower(cyclingPower.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public final class SensorDataSet {
|
||||
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
|
||||
}
|
||||
|
||||
private void set(@NonNull SensorData type, SensorData data) {
|
||||
private void set(@NonNull SensorData<?> type, SensorData<?> data) {
|
||||
if (type instanceof SensorDataHeartRate) {
|
||||
this.heartRate = (SensorDataHeartRate) data;
|
||||
return;
|
||||
|
||||
@@ -224,14 +224,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(TrackPoint trackPoint) {
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
pendingPoints.add(createPendingPoint(trackPoint));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
|
||||
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
if (trackStatisticsUpdater != null) {
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
@@ -240,7 +240,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
|
||||
if (isResumed()) {
|
||||
chartView.addChartPoints(pendingPoints);
|
||||
pendingPoints.clear();
|
||||
|
||||
@@ -198,21 +198,21 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(TrackPoint trackPoint) {
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
|
||||
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
|
||||
if (isResumed()) {
|
||||
runOnUiThread(this::loadIntervals);
|
||||
}
|
||||
|
||||
@@ -27,15 +27,23 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a recorded {@link Track}.
|
||||
@@ -43,36 +51,10 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
|
||||
public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
private static final String TRACK_ID_KEY = "trackId";
|
||||
|
||||
private TrackStatistics trackStatistics;
|
||||
private String category = "";
|
||||
private Track.Id trackId;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private StatisticsRecordedBinding viewBinding;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
||||
@@ -82,18 +64,71 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
private TrackStatistics trackStatistics;
|
||||
private SensorStatistics sensorStatistics;
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
private String category = "";
|
||||
private Track.Id trackId;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private StatisticsRecordedBinding viewBinding;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private boolean preferenceMetricUnits;
|
||||
private boolean preferenceReportSpeed;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
boolean updateUInecessary = false;
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||
}
|
||||
|
||||
if (key != null && updateUInecessary && isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
trackId = getArguments().getParcelable(TRACK_ID_KEY);
|
||||
contentProviderUtils = new ContentProviderUtils(getContext());
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
|
||||
|
||||
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||
sensorsRecyclerView.setAdapter(sensorsAdapter);
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
loadStatistics();
|
||||
}
|
||||
@@ -103,7 +138,13 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
|
||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
sharedPreferences = null;
|
||||
}
|
||||
|
||||
public void loadStatistics() {
|
||||
@@ -112,21 +153,26 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
if (isResumed()) {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
trackStatistics = track != null ? track.getTrackStatistics() : null;
|
||||
category = track != null ? track.getCategory() : "";
|
||||
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
|
||||
String newCategory = track != null ? track.getCategory() : "";
|
||||
if (!category.equals(newCategory)) {
|
||||
category = newCategory;
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
|
||||
}
|
||||
|
||||
updateUI();
|
||||
updateSensorUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
||||
|
||||
// Set total distance
|
||||
{
|
||||
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
|
||||
|
||||
viewBinding.statsDistanceValue.setText(parts.first);
|
||||
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||
@@ -147,9 +193,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
// Set average speed/pace
|
||||
{
|
||||
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
|
||||
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -158,9 +204,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
{
|
||||
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
|
||||
|
||||
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -169,9 +215,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
{
|
||||
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
|
||||
|
||||
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -187,13 +233,35 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
Pair<String, String> parts;
|
||||
|
||||
parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
|
||||
parts = StringUtils.formatElevation(getContext(), elevationGain_m, preferenceMetricUnits);
|
||||
viewBinding.statsElevationGainValue.setText(parts.first);
|
||||
viewBinding.statsElevationGainUnit.setText(parts.second);
|
||||
|
||||
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, metricUnits);
|
||||
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, preferenceMetricUnits);
|
||||
viewBinding.statsElevationLossValue.setText(parts.first);
|
||||
viewBinding.statsElevationLossUnit.setText(parts.second);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSensorUI() {
|
||||
if (sensorStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorStatistics.hasHeartRate()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
|
||||
}
|
||||
if (sensorStatistics.hasCadence()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
|
||||
}
|
||||
if (sensorStatistics.hasPower()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
|
||||
}
|
||||
if (sensorDataList.size() > 0) {
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+118
-95
@@ -10,6 +10,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -26,18 +27,17 @@ import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a currently recording {@link Track}.
|
||||
@@ -45,54 +45,78 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO isRecording should not be relevant anymore as we now have StatisticRecordedFragment.
|
||||
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
|
||||
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
|
||||
|
||||
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
|
||||
public static Fragment newInstance() {
|
||||
return new StatisticsRecordingFragment();
|
||||
}
|
||||
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
private Handler handlerUpdateUI;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||
|
||||
private TrackPoint lastTrackPoint;
|
||||
|
||||
private TrackStatistics lastTrackStatistics;
|
||||
|
||||
private String category = "";
|
||||
|
||||
private StatisticsRecordingBinding viewBinding;
|
||||
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
|
||||
public static Fragment newInstance() {
|
||||
return new StatisticsRecordingFragment();
|
||||
}
|
||||
private SharedPreferences sharedPreferences;
|
||||
private boolean preferenceMetricUnits;
|
||||
private boolean preferenceReportSpeed;
|
||||
private boolean preferenceShowElevation;
|
||||
private boolean preferenceShowCoordinate;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
boolean updateUInecessary = false;
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_elevation_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceShowElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (key != null && updateUInecessary && isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
private final Runnable updateUIeachSecond = new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed() && isSelectedTrackRecording()) {
|
||||
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
|
||||
synchronized (this) {
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
|
||||
lastTrackPoint = null;
|
||||
setLocationValues();
|
||||
}
|
||||
}
|
||||
updateTotalTime();
|
||||
updateSensorDataUI();
|
||||
}
|
||||
@@ -103,27 +127,38 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handlerUpdateUI = new Handler();
|
||||
|
||||
|
||||
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
||||
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
|
||||
|
||||
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||
sensorsRecyclerView.setAdapter(sensorsAdapter);
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
|
||||
updateUIeachSecond.run();
|
||||
handlerUpdateUI.post(updateUIeachSecond);
|
||||
}
|
||||
|
||||
@@ -131,7 +166,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
|
||||
}
|
||||
@@ -152,6 +188,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection = null;
|
||||
sharedPreferences = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,7 +197,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
|
||||
category = track != null ? track.getCategory() : "";
|
||||
String newCategory = track != null ? track.getCategory() : "";
|
||||
if (!category.equals(newCategory)) {
|
||||
category = newCategory;
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
@@ -173,33 +214,32 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(TrackPoint trackPoint) {
|
||||
lastTrackPoint = trackPoint;
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
|
||||
lastTrackPoint = trackPoint;
|
||||
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
|
||||
lastTrackPoint = null;
|
||||
}
|
||||
synchronized (this) {
|
||||
this.lastTrackPoint = newLastTrackPoint;
|
||||
|
||||
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
|
||||
if (trackPoint != null && trackPoint.hasLocation()) {
|
||||
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
|
||||
|
||||
if (!hasFix) {
|
||||
lastTrackPoint = null;
|
||||
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
|
||||
this.lastTrackPoint = null;
|
||||
}
|
||||
|
||||
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
|
||||
this.lastTrackPoint = null;
|
||||
}
|
||||
setLocationValues();
|
||||
}
|
||||
setLocationValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -272,15 +312,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
} else {
|
||||
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
|
||||
if (sensorDataSet != null) {
|
||||
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorDataSet.getHeartRate() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
||||
}
|
||||
if (sensorDataSet.getCyclingCadence() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
||||
}
|
||||
if(sensorDataSet.getCyclingPower() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
|
||||
if (sensorDataSet.getCyclingPower() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
||||
}
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
setSpeedSensorData(sensorDataSet);
|
||||
@@ -293,8 +333,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
|
||||
// Set elevation gain
|
||||
private void setTotalElevationGain(Float elevationGain_m) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
|
||||
Float totalElevationGain = elevationGain_m;
|
||||
|
||||
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) {
|
||||
@@ -305,15 +343,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, preferenceMetricUnits);
|
||||
viewBinding.statsElevationGainValue.setText(parts.first);
|
||||
viewBinding.statsElevationGainUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set elevation loss
|
||||
private void setTotalElevationLoss(Float elevationLoss_m) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
|
||||
Float totalElevationLoss = elevationLoss_m;
|
||||
|
||||
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationLoss()) {
|
||||
@@ -324,7 +360,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, preferenceMetricUnits);
|
||||
viewBinding.statsElevationLossValue.setText(parts.first);
|
||||
viewBinding.statsElevationLossUnit.setText(parts.second);
|
||||
}
|
||||
@@ -333,8 +369,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
|
||||
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
|
||||
|
||||
if (data.hasSpeed_mps() && data.isRecent()) {
|
||||
setSpeed(data.getSpeed_mps());
|
||||
if (data.hasValue() && data.isRecent()) {
|
||||
setSpeed(data.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,14 +378,10 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
private void updateStats() {
|
||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
|
||||
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
||||
boolean isRecording = isSelectedTrackRecording();
|
||||
|
||||
// Set total distance
|
||||
{
|
||||
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
|
||||
|
||||
viewBinding.statsDistanceValue.setText(parts.first);
|
||||
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||
@@ -357,7 +389,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
|
||||
// Set activity type
|
||||
{
|
||||
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
|
||||
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
|
||||
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
||||
}
|
||||
|
||||
@@ -370,9 +402,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
// Set average speed/pace
|
||||
{
|
||||
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
|
||||
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -381,9 +413,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
{
|
||||
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
|
||||
|
||||
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -392,18 +424,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
{
|
||||
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
|
||||
|
||||
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set elevation gain and loss
|
||||
{
|
||||
// Make elevation visible?
|
||||
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
||||
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,46 +450,39 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
private void setLocationValues() {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
|
||||
// Set speed/pace
|
||||
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
|
||||
setSpeed(speed);
|
||||
|
||||
// Set elevation
|
||||
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
||||
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (showElevation) {
|
||||
if (preferenceShowElevation) {
|
||||
// Current elevation
|
||||
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, preferenceMetricUnits);
|
||||
viewBinding.statsElevationCurrentValue.setText(parts.first);
|
||||
viewBinding.statsElevationCurrentUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set coordinate
|
||||
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
|
||||
|
||||
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
|
||||
if (showCoordinate) {
|
||||
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
|
||||
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
|
||||
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
|
||||
if (preferenceShowCoordinate) {
|
||||
String latitudeText = getContext().getString(R.string.value_unknown);
|
||||
String longitudeText = getContext().getString(R.string.value_unknown);
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
|
||||
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
|
||||
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
|
||||
}
|
||||
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||
|
||||
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
|
||||
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
|
||||
viewBinding.statsLongitudeValue.setText(longitudeText);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSpeed(double speed) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
||||
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||
|
||||
viewBinding.statsSpeedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsSpeedValue.setText(parts.first);
|
||||
viewBinding.statsSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML.
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to GPX.
|
||||
* NOTE:
|
||||
* * does not export {@link TrackPoint} without a latitude/longitude (not supported by GPX 1.1).
|
||||
* * cannot export multiple {@link Track}s - all {@link TrackPoint}s are exported as if they would belong to the first track.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
@@ -85,13 +86,21 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
Track mainTrack = tracks[0];
|
||||
try {
|
||||
prepare(outputStream);
|
||||
writeHeader(track);
|
||||
writeHeader(mainTrack);
|
||||
|
||||
writeMarkers(track);
|
||||
writeMarkers(mainTrack);
|
||||
|
||||
writeLocations(track);
|
||||
for (Track track : tracks) {
|
||||
writeLocations(track);
|
||||
}
|
||||
|
||||
writeFooter();
|
||||
close();
|
||||
@@ -103,11 +112,6 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private void writeLocations(Track track) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
@@ -119,7 +123,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
|
||||
if (!wroteTrack) {
|
||||
writeBeginTrack(track, trackPoint);
|
||||
writeBeginTrack(track);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
@@ -158,14 +162,12 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
writeCloseSegment();
|
||||
}
|
||||
|
||||
if (wroteTrack) {
|
||||
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
if (!wroteTrack) {
|
||||
// Write an empty track
|
||||
writeBeginTrack(track, null);
|
||||
writeEndTrack(track, null);
|
||||
writeBeginTrack(track);
|
||||
}
|
||||
|
||||
writeEndTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +245,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
|
||||
public void writeBeginTrack(Track track) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<trk>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
|
||||
@@ -257,7 +259,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
|
||||
public void writeEndTrack() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</trk>");
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.DescriptionGenerator;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -54,8 +53,6 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private static final String TAG = KMLTrackExporter.class.getSimpleName();
|
||||
|
||||
public static final String MARKER_STYLE = "waypoint";
|
||||
private static final String START_STYLE = "start";
|
||||
private static final String END_STYLE = "end";
|
||||
private static final String TRACK_STYLE = "track";
|
||||
private static final String SCHEMA_ID = "schema";
|
||||
|
||||
@@ -67,15 +64,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
public static final String EXTENDED_DATA_TYPE_ELEVATION_LOSS = "elevation_loss";
|
||||
|
||||
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
|
||||
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
|
||||
private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
|
||||
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
|
||||
|
||||
private final Context context;
|
||||
private final boolean exportPhotos;
|
||||
private final boolean exportTrackDetail;
|
||||
private final boolean exportSensorData;
|
||||
private final DescriptionGenerator descriptionGenerator;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private PrintWriter printWriter;
|
||||
@@ -86,6 +80,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private final List<Float> elevationGainList = new ArrayList<>();
|
||||
private final List<Float> elevationLossList = new ArrayList<>();
|
||||
|
||||
@Deprecated // Figure out a better way to do this! (if needed)
|
||||
private TrackPoint startTrackPoint;
|
||||
|
||||
public KMLTrackExporter(Context context, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
|
||||
@@ -93,7 +88,6 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
this.exportTrackDetail = exportTrackDetail;
|
||||
this.exportSensorData = exportSensorData;
|
||||
this.exportPhotos = exportPhotos;
|
||||
this.descriptionGenerator = new DescriptionGenerator(context);
|
||||
this.contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
@@ -165,9 +159,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
setLocationTime(trackPoint, offset);
|
||||
if (startTrackPoint == null) {
|
||||
startTrackPoint = trackPoint;
|
||||
}
|
||||
|
||||
if (!wroteTrack) {
|
||||
writeBeginTrack(track, trackPoint);
|
||||
writeBeginTrack(track);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
@@ -202,15 +199,14 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
writeCloseSegment();
|
||||
}
|
||||
|
||||
if (wroteTrack) {
|
||||
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
setLocationTime(lastValidTrackPoint, offset);
|
||||
writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
if (!wroteTrack) {
|
||||
// Write an empty track
|
||||
writeBeginTrack(track, null);
|
||||
writeEndTrack(track, null);
|
||||
writeBeginTrack(track);
|
||||
}
|
||||
|
||||
writeEndTrack();
|
||||
|
||||
startTrackPoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,8 +243,6 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
writeTrackStyle();
|
||||
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
|
||||
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
|
||||
writePlacemarkerStyle(MARKER_STYLE, MARKER_ICON, 20, 2);
|
||||
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
|
||||
|
||||
@@ -312,12 +306,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
|
||||
this.startTrackPoint = startTrackPoint;
|
||||
private void writeBeginTrack(Track track) {
|
||||
if (printWriter != null) {
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
Location location = startTrackPoint != null ? startTrackPoint.getLocation() : null;
|
||||
writePlacemark(name, "", "", START_STYLE, location);
|
||||
printWriter.println("<Placemark>");
|
||||
|
||||
if (exportTrackDetail) {
|
||||
@@ -336,17 +326,10 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
|
||||
private void writeEndTrack(Track track, TrackPoint endTrackPoint) {
|
||||
private void writeEndTrack() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gx:MultiTrack>");
|
||||
printWriter.println("</Placemark>");
|
||||
|
||||
if (exportTrackDetail) {
|
||||
String name = context.getString(R.string.marker_label_end, track.getName());
|
||||
String description = descriptionGenerator.generateTrackDescription(track, false);
|
||||
Location location = endTrackPoint != null ? endTrackPoint.getLocation() : null;
|
||||
writePlacemark(name, "", description, END_STYLE, location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-16
@@ -21,6 +21,8 @@ import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
@@ -122,7 +124,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
try {
|
||||
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
|
||||
return trackIds.get(0);
|
||||
} catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
} catch (IOException | SAXException | ParserConfigurationException | ParsingException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
if (trackIds.size() > 0) {
|
||||
cleanImport();
|
||||
@@ -304,7 +306,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
}
|
||||
}
|
||||
|
||||
protected void addMarker() throws SAXException {
|
||||
protected void addMarker() throws ParsingException {
|
||||
// Markers must have a time, else cannot match to the track points
|
||||
if (time == null) {
|
||||
Log.w(TAG, "Marker without time ignored.");
|
||||
@@ -335,7 +337,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
markers.add(marker);
|
||||
}
|
||||
|
||||
protected TrackPoint getTrackPoint() throws SAXException {
|
||||
protected TrackPoint getTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
@@ -350,7 +352,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
trackPoint.setTime(time);
|
||||
|
||||
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,16 +426,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a location.
|
||||
*/
|
||||
private TrackPoint createTrackPoint() throws SAXException {
|
||||
private TrackPoint createTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
try {
|
||||
trackPoint.setTime(StringUtils.parseTime(time));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
@@ -444,7 +443,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
trackPoint.setLatitude(Double.parseDouble(latitude));
|
||||
trackPoint.setLongitude(Double.parseDouble(longitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
|
||||
|
||||
@@ -452,7 +451,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
try {
|
||||
trackPoint.setAltitude(Double.parseDouble(altitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,14 +459,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
try {
|
||||
trackPoint.setSpeed(Float.parseFloat(speed));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
|
||||
}
|
||||
}
|
||||
if (heartrate != null) {
|
||||
try {
|
||||
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +474,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
try {
|
||||
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,14 +482,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
try {
|
||||
trackPoint.setElevationGain(Float.parseFloat(gain));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
|
||||
}
|
||||
}
|
||||
if (loss != null) {
|
||||
try {
|
||||
trackPoint.setElevationLoss(Float.parseFloat(loss));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,4 +545,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
// The buffered locations
|
||||
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static class ParsingException extends RuntimeException {
|
||||
|
||||
private ParsingException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
private ParsingException(@NonNull String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,33 +57,33 @@ public class ImportService extends JobIntentService {
|
||||
trackImporter = new KmzTrackImporter(this, file.getUri());
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file format.");
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unsupported_format));
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
|
||||
return;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
|
||||
Track.Id trackId = trackImporter.importFile(inputStream);
|
||||
if (trackId != null) {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file.getName(), getString(R.string.import_file_imported, file.getName()));
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file, getString(R.string.import_file_imported, file.getName()));
|
||||
} else {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file.getName(), getString(R.string.import_unable_to_import_file, file.getName()));
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file, getString(R.string.import_unable_to_import_file, file.getName()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to import file", e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unable_to_import_file, e.getMessage()));
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unable_to_import_file, e.getMessage()));
|
||||
} catch (ImportParserException e) {
|
||||
Log.d(TAG, "Parser error: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_parser_error, e.getMessage()));
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_parser_error, e.getMessage()));
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file.getName(), e.getMessage());
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResult(int resultCode, Track.Id trackId, String fileName, String message) {
|
||||
private void sendResult(int resultCode, Track.Id trackId, DocumentFile file, String message) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, fileName);
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName());
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
|
||||
resultReceiver.send(resultCode, bundle);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
|
||||
private int successCount;
|
||||
private int existsCount;
|
||||
private int errorCount;
|
||||
private ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
|
||||
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
|
||||
private final ArrayList<String> fileErrors = new ArrayList<>();
|
||||
|
||||
public int getTotalCount() {
|
||||
|
||||
@@ -66,7 +66,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.SystemUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackNameUtils;
|
||||
import de.dennisguse.opentracks.util.TrackPointUtils;
|
||||
|
||||
/**
|
||||
* A background service that registers a location listener and records track points.
|
||||
@@ -594,7 +593,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
|
||||
if (!isIdle && !trackPoint.isMoving()) {
|
||||
insertTrackPointIfNewer(track, lastTrackPoint);
|
||||
|
||||
insertTrackPoint(track, trackPoint);
|
||||
@@ -605,7 +604,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
|
||||
if (isIdle && trackPoint.isMoving()) {
|
||||
insertTrackPointIfNewer(track, lastTrackPoint);
|
||||
|
||||
insertTrackPoint(track, trackPoint);
|
||||
|
||||
@@ -16,7 +16,6 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.TrackPointUtils;
|
||||
|
||||
class LocationHandler implements HandlerServer.Handler, LocationListener, GpsStatus.GpsStatusListener {
|
||||
|
||||
@@ -92,7 +91,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
|
||||
@Override
|
||||
public void onLocationChanged(@NonNull Location location) {
|
||||
TrackPoint trackPoint = new TrackPoint(location);
|
||||
boolean isAccurate = TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy);
|
||||
boolean isAccurate = trackPoint.fulfillsAccuracy(recordingGpsAccuracy);
|
||||
boolean isValid = LocationUtils.isValidLocation(location);
|
||||
|
||||
if (gpsStatus != null) {
|
||||
|
||||
@@ -94,7 +94,7 @@ public class PeriodicTaskExecutor {
|
||||
if (timerTaskExecutor == null) {
|
||||
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
|
||||
}
|
||||
timerTaskExecutor.scheduleTask(Duration.ofSeconds(taskFrequency));
|
||||
timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency));
|
||||
} else {
|
||||
// For distance periodic task
|
||||
calculateNextTaskDistance();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
public class SensorStatistics {
|
||||
private final Float maxHr;
|
||||
private final Float avgHr;
|
||||
private final Float maxCadence;
|
||||
private final Float avgCadence;
|
||||
private final Float avgPower;
|
||||
|
||||
public SensorStatistics(Float maxHr, Float avgHr, Float maxCadence, Float avgCadence, Float avgPower) {
|
||||
this.maxHr = maxHr;
|
||||
this.avgHr = avgHr;
|
||||
this.maxCadence = maxCadence;
|
||||
this.avgCadence = avgCadence;
|
||||
this.avgPower = avgPower;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return avgHr != null && maxHr != null;
|
||||
}
|
||||
|
||||
public float getMaxHeartRate() {
|
||||
return maxHr;
|
||||
}
|
||||
|
||||
public float getAvgHeartRate() {
|
||||
return avgHr;
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return avgCadence != null && maxCadence != null;
|
||||
}
|
||||
|
||||
public float getMaxCadence() {
|
||||
return maxCadence;
|
||||
}
|
||||
|
||||
public float getAvgCadence() {
|
||||
return avgCadence;
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return avgPower != null;
|
||||
}
|
||||
|
||||
public float getAvgPower() {
|
||||
return avgPower;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.TrackPointUtils;
|
||||
|
||||
/**
|
||||
* Updater for {@link TrackStatistics}.
|
||||
@@ -161,7 +160,7 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
|
||||
if (movingDistance < minRecordingDistance && !TrackPointUtils.isMoving(trackPoint)) {
|
||||
if (movingDistance < minRecordingDistance && !trackPoint.isMoving()) {
|
||||
speedBuffer_ms.reset();
|
||||
lastTrackPoint = trackPoint;
|
||||
return;
|
||||
@@ -211,7 +210,7 @@ public class TrackStatisticsUpdater {
|
||||
*/
|
||||
@VisibleForTesting
|
||||
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
||||
if (!TrackPointUtils.isMoving(trackPoint)) {
|
||||
if (!trackPoint.isMoving()) {
|
||||
speedBuffer_ms.reset();
|
||||
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
|
||||
speedBuffer_ms.setNext(trackPoint.getSpeed());
|
||||
|
||||
@@ -82,29 +82,8 @@ public class AnnouncementUtils {
|
||||
return result + minutesText + " " + secondsText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString.
|
||||
* getQuantityString only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer quantity.
|
||||
* If the double quantity is exactly 0, 1, or 2, then we can return these integer quantities.
|
||||
* Otherwise, we cast the double quantity to an integer quantity.
|
||||
* However, we need to make sure that if the casted value is 0, 1, or 2, we don't return those, instead, return the next biggest integer 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
private static int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
|
||||
int count = (int) d;
|
||||
return Math.max(count, 3);
|
||||
}
|
||||
static int getQuantityCount(double d) {
|
||||
return (int) d;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
/**
|
||||
* Utility class for decimating tracks at a given level of precision.
|
||||
*
|
||||
@@ -31,14 +26,11 @@ public class LocationUtils {
|
||||
|
||||
private static final String TAG = LocationUtils.class.getSimpleName();
|
||||
|
||||
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
|
||||
|
||||
private LocationUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given location is a valid (i.e. physically possible) location on Earth.
|
||||
* Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble.
|
||||
*
|
||||
* @param location the location to test
|
||||
* @return true if the location is a valid location.
|
||||
@@ -48,10 +40,4 @@ public class LocationUtils {
|
||||
&& Math.abs(location.getLatitude()) <= 90
|
||||
&& Math.abs(location.getLongitude()) <= 180;
|
||||
}
|
||||
|
||||
public static boolean isTrackPointOld(TrackPoint trackPoint) {
|
||||
return Duration.between(trackPoint.getTime(), Instant.now())
|
||||
.minus(MAX_LOCATION_AGE)
|
||||
.isNegative();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ public class PreferencesUtils {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||
}
|
||||
|
||||
public static void register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
|
||||
getSharedPreferences(context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
public static SharedPreferences register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
public static void unregister(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
|
||||
@@ -106,6 +108,10 @@ public class PreferencesUtils {
|
||||
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
private static boolean getBoolean(Context context, SharedPreferences sharedPreferences, int keyId, boolean defaultValue) {
|
||||
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a boolean preference value.
|
||||
*
|
||||
@@ -179,18 +185,16 @@ public class PreferencesUtils {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string preference value.
|
||||
*
|
||||
* @param context the context
|
||||
* @param keyId the key id
|
||||
* @param defaultValue default value
|
||||
*/
|
||||
@Deprecated
|
||||
public static String getString(Context context, int keyId, String defaultValue) {
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
public static String getString(Context context, SharedPreferences sharedPreferences, int keyId, String defaultValue) {
|
||||
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a string preference value.
|
||||
*
|
||||
@@ -206,15 +210,25 @@ public class PreferencesUtils {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static boolean isMetricUnits(Context context) {
|
||||
final String STATS_UNIT = context.getString(R.string.stats_units_default);
|
||||
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
|
||||
return isMetricUnits(getSharedPreferences(context), context);
|
||||
}
|
||||
|
||||
public static boolean isMetricUnits(SharedPreferences sharedPreferences, Context context) {
|
||||
final String STATS_UNIT = context.getString(R.string.stats_units_default);
|
||||
return STATS_UNIT.equals(getString(context, sharedPreferences, R.string.stats_units_key, STATS_UNIT));
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static boolean isReportSpeed(Context context, String category) {
|
||||
return isReportSpeed(getSharedPreferences(context), context, category);
|
||||
}
|
||||
|
||||
public static boolean isReportSpeed(SharedPreferences sharedPreferences, Context context, String category) {
|
||||
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
|
||||
String currentStatsRate = getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
|
||||
if (currentStatsRate.equals(getString(context, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
|
||||
String currentStatsRate = getString(context, sharedPreferences, R.string.stats_rate_key, STATS_RATE_DEFAULT);
|
||||
if (currentStatsRate.equals(getString(context, sharedPreferences, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
|
||||
return TrackIconUtils.isSpeedIcon(context, category);
|
||||
}
|
||||
|
||||
@@ -286,12 +300,17 @@ public class PreferencesUtils {
|
||||
return getBoolean(context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static boolean isShowStatsElevation(Context context) {
|
||||
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
|
||||
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
|
||||
return isShowStatsElevation(getSharedPreferences(context), context);
|
||||
}
|
||||
|
||||
public static boolean isStatsShowCoordinate(Context context) {
|
||||
public static boolean isShowStatsElevation(SharedPreferences sharedPreferences, Context context) {
|
||||
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
|
||||
return getBoolean(context, sharedPreferences, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
|
||||
}
|
||||
|
||||
public static boolean isStatsShowCoordinate(SharedPreferences sharedPreferences, Context context) {
|
||||
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
|
||||
return getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
public class TrackPointUtils {
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
private final static String TAG = TrackPointUtils.class.getSimpleName();
|
||||
|
||||
private TrackPointUtils() {
|
||||
}
|
||||
|
||||
public static boolean isMoving(@NonNull TrackPoint trackPoint) {
|
||||
return trackPoint.hasSpeed() && trackPoint.getSpeed() >= MAX_NO_MOVEMENT_SPEED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is accuracy better than threshold?
|
||||
*/
|
||||
public static boolean fulfillsAccuracy(@NonNull TrackPoint trackPoint, int poorAccuracy) {
|
||||
return trackPoint.hasAccuracy() && trackPoint.getAccuracy() < poorAccuracy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class SensorDataModel {
|
||||
private int labelId;
|
||||
private final String sensorValue;
|
||||
private String sensorName;
|
||||
private int unitId;
|
||||
|
||||
public SensorDataModel(int labelId, int unitId, float sensorValue) {
|
||||
this.labelId = labelId;
|
||||
this.unitId = unitId;
|
||||
this.sensorValue = StringUtils.formatDecimal(sensorValue, 0);
|
||||
}
|
||||
|
||||
public SensorDataModel(SensorData sensorData) {
|
||||
this.sensorName = sensorData.getSensorNameOrAddress();
|
||||
this.sensorValue = sensorData.hasValue() && sensorData.isRecent() ? StringUtils.formatDecimal((float) sensorData.getValue(), 0) : null;
|
||||
if (sensorData instanceof SensorDataHeartRate) {
|
||||
this.labelId = R.string.sensor_state_heart_rate;
|
||||
this.unitId = R.string.sensor_unit_beats_per_minute;
|
||||
} else if (sensorData instanceof SensorDataCycling.Cadence) {
|
||||
this.labelId = R.string.sensor_state_cadence;
|
||||
this.unitId = R.string.sensor_unit_rounds_per_minute;
|
||||
} else if (sensorData instanceof SensorDataCyclingPower) {
|
||||
this.labelId = R.string.sensor_state_power;
|
||||
this.unitId = R.string.sensor_unit_power;
|
||||
}
|
||||
}
|
||||
|
||||
public int getLabelId() {
|
||||
return labelId;
|
||||
}
|
||||
|
||||
public int getUnitId() {
|
||||
return unitId;
|
||||
}
|
||||
|
||||
public String getSensorName() {
|
||||
return sensorName;
|
||||
}
|
||||
|
||||
public String getSensorValue() {
|
||||
return sensorValue;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return sensorValue != null;
|
||||
}
|
||||
}
|
||||
@@ -313,5 +313,14 @@
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_barrier" />
|
||||
|
||||
<!-- Sensors data -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/stats_sensors_recycler_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_end_horizontal_line" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
@@ -111,7 +111,7 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
|
||||
<string name="activity_type_skate_boarding">skateboarding</string>
|
||||
<string name="activity_type_skating">bruslení</string>
|
||||
<string name="activity_type_skiing">lyžování</string>
|
||||
<string name="activity_type_sky_jumping">sky jumping</string>
|
||||
<string name="activity_type_sky_jumping">skákání na obloze</string>
|
||||
<string name="activity_type_sled">sáně</string>
|
||||
<string name="activity_type_snow_boarding">snowboarding</string>
|
||||
<string name="activity_type_snow_shoeing">lyžování na sněžnicích</string>
|
||||
@@ -594,4 +594,18 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
|
||||
<item quantity="few">Ukončeno s %1$d chybami</item>
|
||||
<item quantity="other">Ukončeno s %1$d chybami</item>
|
||||
</plurals>
|
||||
<string name="import_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Zabraňte opětovnému importu Stop</string>
|
||||
<string name="export_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="export_progress_summary_skip_msg">Stopy přeskočeny</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Skladby přepsány</string>
|
||||
<string name="export_progress_summary_new_msg">Vyvezeny nové tratě</string>
|
||||
<string name="import_progress_summary_exists_msg">Existovat</string>
|
||||
<string name="import_progress_summary_ok_msg">Importované soubory</string>
|
||||
<string name="generic_open_track">Otevřená Trať</string>
|
||||
<string name="sensor_state_power_avg">Výkon Avg</string>
|
||||
<string name="sensor_state_heart_rate_max">Maximální srdeční frekvence</string>
|
||||
<string name="sensor_state_heart_rate_avg">Avg srdeční frekvence</string>
|
||||
<string name="sensor_state_cadence_max">Max kadence</string>
|
||||
<string name="sensor_state_cadence_avg">Avg kadence</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2010 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
--><resources>
|
||||
<string name="about_preference_title">Om OpenTracks</string>
|
||||
<string name="about_description"><i> OpenTracks </i> er en sportssporingsapplikation, der fuldt ud respekterer dit privatliv.</string>
|
||||
<string name="about_url">Projektwebside: <a href="%1$s">%1$s</a></string>
|
||||
@@ -30,40 +30,28 @@ limitations under the License.
|
||||
* Opret markører (inkl. billeder) \n
|
||||
* fungerer helt offline \n
|
||||
* vis spor på et kort (se nedenfor)</string>
|
||||
|
||||
<string name="help_track_map_title">Hvordan viser man et spor på et kort?</string>
|
||||
<string name="help_track_map_content">
|
||||
Brug det lille kortikon. \n
|
||||
a)
|
||||
(liveopdateringer) Installer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>.
|
||||
Det giver online eller offline kort via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.\n
|
||||
Faktisk er OSMDashboard en separat app, der kun kan få adgang til (midlertidigt) adgang til dataene fra spor (e), der skal vises.\n
|
||||
b) Via et normalt kortprogram (f.eks. <a href="http://osmand.net/">OsmAnd</a> og <a href="https://maps.me/">MAPS.ME</a>).
|
||||
Disse app skal understøtte KMZ-filformatet (kun placeringer og tidsstempler).
|
||||
Faktisk skal en app understøtte <i>ACTION_VIEW</i> med MIME <i>application/vnd.google-earth.kmz</i>.</string>
|
||||
|
||||
<string name="help_track_map_content">" Brug det lille kortikon.
|
||||
\n a) (live opdateringer) Installer <a href=\"https://github.com/OpenTracksApp/OSMDashboard\"> OSMDashboard </a>. Det giver online eller offline kort via <a href=\"https://www.openstreetmap.org\"> OpenStreetMap </a>.
|
||||
\n Faktisk er OSMDashboard en separat app, der kun kan få adgang (midlertidigt) til dataene for spor, der skal vises.
|
||||
\n b) Via en normal kortapplikation (f.eks. <a href=\"http://osmand.net/\"> OsmAnd </a> og <a href=\"https://maps.me/\"> MAPS.ME < / a>). Disse apps skal understøtte KMZ-filformatet (kun placeringer og tidsstempler). Faktisk skal en app understøtte <i> ACTION_VIEW </i> med MIME <i> -applikation / vnd.google-earth.kmz </i>. "</string>
|
||||
<string name="help_storage_pictures_title">Hvordan gemmer OpenTracks billeder, der blev taget som markører?</string>
|
||||
<string name="help_storage_pictures_content">Disse gemmes i appens interne lager (dvs. kun tilgængelig inden for OpenTracks).</string>
|
||||
|
||||
<string name="help_track_export_title">Hvordan kan jeg eksportere / dele indspillede numre?</string>
|
||||
<string name="help_track_export_content">
|
||||
Spor kan eksporteres fra indstillingerne som KMZ, KML eller GPX.
|
||||
KMZ / KML alle data; GPX kun placeringer.
|
||||
Spor, der deles via <i>delingsikonet</i> deles som KMZ.</string>
|
||||
|
||||
<string name="help_data_google_play_services_title">Kræver/bruger OpenTracks Google Play Service?</string>
|
||||
|
||||
<string name="help_recording_title">Hvordan registrerer OpenTracks data?</string>
|
||||
<string name="help_recording_content">
|
||||
OpenTracks bruger enhedens GPS-sensor til at registrere placeringen kontinuerligt.
|
||||
Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, højde), kan OpenTracks ikke gøre noget ved det.</string>
|
||||
|
||||
<string name="help_data_storage_title">Hvordan gemmer OpenTracks sensordata?</string>
|
||||
<string name="help_data_storage_content">
|
||||
Sensordata gemmes pr. Optaget placering.
|
||||
Med hensyn til en pulssensor betyder det, at der kun er <i> en </i> måling af hjertefrekvensen lagret pr. Placering.
|
||||
Hvis du står stille eller er indendørs, registreres ingen sensordata (men de vil blive vist).</string>
|
||||
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks optager ikke placeringer?</string>
|
||||
<string name="help_gps_troubleshooting_content">
|
||||
Hvad du kan gøre:\n
|
||||
@@ -73,7 +61,6 @@ c) Kontroller, om du har appen <a href="https://f-droid.org/en/packages/com.andr
|
||||
d) deaktiver strømbesparelse. \n
|
||||
\n
|
||||
Hvis alt ikke hjælper, skal du rapportere en fejl.</string>
|
||||
|
||||
<string name="help_support_title">Hvordan kan jeg støtte OpenTracks?</string>
|
||||
<string name="help_support_content">
|
||||
Du kan: \n
|
||||
@@ -84,7 +71,6 @@ d) hjælp til softwareudvikling, \n
|
||||
e) køb i <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>, \n
|
||||
f) donere, og \n
|
||||
g) gør hvad du synes hjælper OpenTracks.</string>
|
||||
|
||||
<string name="open_track_using_dashboard">Åben med OpenTracks-Dashboard</string>
|
||||
<string name="open_track_as_KMZ">Åben som KMZ</string>
|
||||
<string name="open_track_as_GPX">Åben som GPX</string>
|
||||
@@ -366,14 +352,12 @@ g) gør hvad du synes hjælper OpenTracks.</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth-sensor</string>
|
||||
<string name="settings_sensor_connecting">Tilsluttet %1$s</string>
|
||||
<string name="settings_sensor_disconnected">Frakoblet %1$s</string>
|
||||
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_coordinate">Breddegrad/længdegrad</string>
|
||||
<string name="settings_stats_rate_title">Foretrukkenfartenhed</string>
|
||||
<string name="settings_stats_units_imperial">Britisk (mil, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrisk (km, m)</string>
|
||||
<string name="settings_stats_units_title">Foretrukne enheder</string>
|
||||
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file">Del rutefil</string>
|
||||
<string name="share_track_share_file_body">Du kunne måske være interesseret i denne rute.\n\nDu kan bruge Google Earth eller anden
|
||||
@@ -565,5 +549,4 @@ g) gør hvad du synes hjælper OpenTracks.</string>
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Jeg vil gerne dele et billede med dig</string>
|
||||
<string name="share_image_body">Jeg tror, du måske er interesseret i dette billede.</string>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2010 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
--><resources>
|
||||
<string name="about_preference_title">Tietoja OpenTracksista</string>
|
||||
<string name="about_description"><i> OpenTracks </i> on urheiluseurantaohjelma, joka kunnioittaa yksityisyyttäsi täysin.</string>
|
||||
<string name="about_url">Hankkeen verkkosivu: <a href="%1$s">%1$s</a></string>
|
||||
@@ -30,54 +30,36 @@ limitations under the License.
|
||||
* luo merkkejä (mukaan lukien kuvia)\n
|
||||
* toimii täysin ilman verkkoyhteyttä\n
|
||||
* näytä jäljet kartalla (ks. alla)</string>
|
||||
|
||||
<string name="help_track_map_title">Kuinka näen jäljen kartalla?</string>
|
||||
<string name="help_track_map_content">
|
||||
|
||||
Käytä karttakuvaketta.\n
|
||||
a)
|
||||
(livepäivitykset) Asenna <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>.
|
||||
Se tarjoaa online- ja offlinekarttoja <a href="https://www.openstreetmap.org">OpenStreetMapista</a>.\n
|
||||
Se on erillinen sovellus, jolla on pääsy ainoastaan sillä hetkellä näytettävään/näytettäviin jälkiin.
|
||||
|
||||
\n
|
||||
b)
|
||||
\"Normaalia\" karttasovellusta käyttäen (esimerkiksi <a href="http://osmand.net/">OsmAnd</a> tai <a href="https://maps.me/">MAPS.ME</a>).
|
||||
Sovelluksen pitää tukea KMZ-tiedostomuotoa (vain sijainnit ja aikaleimat).
|
||||
Sovelluksella pitää olla <i>ACTION_VIEW</i>-tuki MIME-tyypillä <i>application/vnd.google-earth.kmz</i>.</string>
|
||||
|
||||
<string name="help_track_map_content">" UsKäytä pientä karttakuvaketta.
|
||||
\n a) (live-päivityksiä) Asenna <a href=\"https://github.com/OpenTracksApp/OSMDashboard\">OSMDashboard</a>. Se tarjoaa online-tai offline-karttoja kautta <a href=\"https://www.openstreetmap.org\">OpenStreetMap</a>.
|
||||
\n Itse asiassa, OSMDashboard on erillinen sovellus, joka vain voi käyttää (tilapäisesti) tiedot track(s), joka tulee näkyviin.
|
||||
\n b) Kautta tavallinen kartta sovellus (esim. <a href=\"http://osmand.net/\">OsmAnd</a> ja <a href=\"https://maps.me/\">KARTAT.MINULLE</a>). Nämä sovellukset tarvitsevat tukea KMZ-muodossa (vain paikoissa ja aikaleimat). Itse asiassa sovelluksen on tuettava < i>ACTION_VIEW< / i> MIME <i>application / vnd: n kanssa.google-maa.kmz</i>. "</string>
|
||||
<string name="help_storage_pictures_title">Miten OpenTracks tallentaa merkkeihin liittyen otetut kuvat?</string>
|
||||
<string name="help_storage_pictures_content">Ne säilytetään sovelluksen sisäisessä tallennustilassa (eli ne näkyvät vain OpenTracksin sisällä).</string>
|
||||
|
||||
<string name="help_track_export_title">Miten vien tai jaan tallennettuja reittejä?</string>
|
||||
<string name="help_track_export_content">
|
||||
Jäljet voi viedä asetuksista KMZ, KML tai GPX -muodossa.
|
||||
KMZ/KML sisältävät kaiken; GPX vain sijainnit.
|
||||
<i>Jakokuvakkeella</i> jaetut jäljet jaetaan KMZ-muodossa.</string>
|
||||
|
||||
<string name="help_data_google_play_services_title">Käyttääkö tai vaatiiko OpenTracks Google Play -palveluita?</string>
|
||||
|
||||
<string name="help_recording_title">Miten OpenTracks tallentaa tietoja?</string>
|
||||
<string name="help_recording_content">
|
||||
OpenTracks käyttää laitteen GPS:ää paikkatietojen jatkuvaan tallentamiseen.
|
||||
Jos GPS raportoi epätarkkoja tietoja (sijainti, nopeus, korkeus) OpenTracks ei voi tehdä sille mitään.</string>
|
||||
|
||||
<string name="help_data_storage_title">Miten OpenTracks tallentaa anturidatan?</string>
|
||||
<string name="help_data_storage_content">
|
||||
Anturidata tallennetaan jokaiselle paikkapisteelle.
|
||||
Esimerkiksi sykemittarista tallennetaan vain <i>yksi</i> sykemittaus tallennettua paikkaa kohden.
|
||||
Jos olet paikallaan tai sisätiloissa, mitään anturidataa ei tallenneta (mutta se näytetään).</string>
|
||||
|
||||
<string name="help_gps_troubleshooting_title">Miksi OpenTracks ei tallenna sijaintia?</string>
|
||||
<string name="help_gps_troubleshooting_content">
|
||||
Mitä voit tehdä:\n
|
||||
a) tarkista että GPS on käytössä,\n
|
||||
b) paranna GPS-signaalia (esim. mene ulos),\n
|
||||
c) tarkista <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>-sovelluksella, ja\n
|
||||
d) poista virransäästö käytöstä.\n
|
||||
\n
|
||||
Jos mikään muu ei auta, ilmoita ohjelmistovirheestä, kiitos.</string>
|
||||
|
||||
<string name="help_gps_troubleshooting_content"> Mitä voit tehdä:
|
||||
\na) tarkista, onko GPS käytössä,
|
||||
\nb) paranna GPS-vastaanottoa (esim.),
|
||||
\nc) tarkista <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, ja
|
||||
\nd) Poista virransäästö käytöstä.
|
||||
\n
|
||||
\nJos kaikki ei auta, ilmoita virheestä. </string>
|
||||
<string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string>
|
||||
<string name="help_support_content">
|
||||
Voit:\n
|
||||
@@ -88,12 +70,10 @@ limitations under the License.
|
||||
e) ostaa sovelluksen <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play -kaupasta</a>,\n
|
||||
f) lahjoittaa ja
|
||||
g) tehdä mikä mielestäsi edistäisi OpenTracksia.</string>
|
||||
|
||||
<string name="open_track_using_dashboard">Avaa OpenTracks-Dashboardilla</string>
|
||||
<string name="open_track_as_KMZ">Avaa KMZ-muodossa</string>
|
||||
<string name="open_track_as_GPX">Avaa GPX-muodossa</string>
|
||||
<string name="open_track_as_trackfileformat">Avaa %1$s-muodossa</string>
|
||||
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">lentokone</string>
|
||||
<string name="activity_type_atv">mönkijä</string>
|
||||
@@ -324,7 +304,7 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="sensor_state_cadence_value">%1$d kierr./min</string>
|
||||
<string name="sensor_state_disconnected">Yhteys katkaistu</string>
|
||||
<string name="sensor_state_heart_rate">Syke</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d ppm</string>
|
||||
<string name="sensor_state_power">Virransyöttö</string>
|
||||
<string name="sensor_state_power_value">%1$d W</string>
|
||||
<string name="sensor_not_known">Anturi %1$s on tuntematon. Tarkista asetukset.</string>
|
||||
@@ -373,7 +353,6 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth-anturi</string>
|
||||
<string name="settings_sensor_connecting">Yhdistetty: %1$s</string>
|
||||
<string name="settings_sensor_disconnected">Yhteys katkaistu: %1$s</string>
|
||||
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_coordinate">Leveys-/pituusaste</string>
|
||||
<string name="settings_stats_rate_title">Haluttu nopeus</string>
|
||||
@@ -381,11 +360,12 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
(maili, jalka)</string>
|
||||
<string name="settings_stats_units_metric">Metrijärjestelmä (km, m)</string>
|
||||
<string name="settings_stats_units_title">Halutut yksiköt</string>
|
||||
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file">Jaa reittitiedosto</string>
|
||||
<string name="share_track_share_file_body">Luulen, että voisit olla kiinnostunut tästä reitistä.\n\nNäet reitin käyttämällä Google Earth -palvelua tai muuta kartoitusohjelmistoa.\n\n%1$s</string>
|
||||
<string name="share_track_subject">Haluaisin jakaa reitin kanssasi.</string>
|
||||
<string name="share_track_share_file_body">Luulen, että saatat olla kiinnostunut tästä kappaleesta.
|
||||
\n
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">Haluaisin jakaa kappaleen kanssasi</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">Keskim. liikkumisvauhti</string>
|
||||
<string name="stats_average_moving_speed">Keskim. liikkumisnopeus</string>
|
||||
@@ -574,5 +554,32 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Haluaisin jakaa kuvan kanssasi</string>
|
||||
<string name="share_image_body">Luulen, että voisit olla kiinnostunut tästä kuvasta.</string>
|
||||
|
||||
</resources>
|
||||
<string name="stats_split_interval">Jakoväli</string>
|
||||
<string name="stats_loss">Tappio</string>
|
||||
<string name="track_detail_intervals_tab">Välein</string>
|
||||
<string name="voice_pace_lap">Kierrosaika %1$s</string>
|
||||
<string name="instant_export_enabled_title">Instant post-workout vienti</string>
|
||||
<string name="toast_message_instant_track_was_exported">Viimeinen kappale viety</string>
|
||||
<string name="hold_to_pause">Pidä tauko</string>
|
||||
<string name="hold_to_stop">Pidä lopettaa</string>
|
||||
<string name="gps_fixed_and_ready">GPS kiinteä ja valmis</string>
|
||||
<string name="import_no_kml_file_found">Ei doc.KML tiedosto löytyy KMZ</string>
|
||||
<string name="settings_default_export_uri_title">Seuraa vientihakemistoa</string>
|
||||
<string name="instant_export_enabled_summary">Vie raita varastoon tallennuksen jälkeen on valmis</string>
|
||||
<string name="voice_speed_lap">Kierrosnopeus %1$s</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="settings_sensor_wheel_circumference">Pyörän koko (mm)</string>
|
||||
<string name="stats_intervals">Välein</string>
|
||||
<string name="import_thread_interrupted">Lanka keskeytetty</string>
|
||||
<string name="import_unsupported_format">Tukematon tiedostomuoto</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Estä kappaleiden uudelleen tuonti</string>
|
||||
<string name="import_parser_error">Jäsentimen virhe: %1$s</string>
|
||||
<string name="import_unable_to_import_file">Tiedostoa ei voi tuoda: %1$s</string>
|
||||
<string name="import_error_info">%1$s: %2$s</string>
|
||||
<string name="import_error_list_dialog_title">Tiedostoja ei tuotu</string>
|
||||
<string name="import_file_imported">Tiedosto %1$s tuotu</string>
|
||||
<string name="import_prevent_reimport">Olet asettanut Estä uudelleen tuonti raita</string>
|
||||
<string name="import_progress_summary_ok_msg">Tuodut tiedostot</string>
|
||||
<string name="import_progress_summary_exists_msg">Jo olemassa</string>
|
||||
<string name="import_progress_summary_errors_msg">Virhe</string>
|
||||
</resources>
|
||||
@@ -356,7 +356,7 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||
<string name="settings_sensor_connecting">Conectado a %1$s</string>
|
||||
<string name="settings_sensor_disconnected">Desconectado de %1$s</string>
|
||||
<string name="settings_sensor_wheel_circumference">Tamaño da roda (mm)</string>
|
||||
<string name="settings_sensor_wheel_circumference">Circunferencia da roda (mm)</string>
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_coordinate">Latitude/lonxitude</string>
|
||||
<string name="settings_stats_rate_title">Indicador preferido</string>
|
||||
@@ -611,4 +611,9 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="import_progress_summary_ok_msg">Ficheiros importados</string>
|
||||
<string name="import_progress_summary_exists_msg">Xa existe</string>
|
||||
<string name="import_progress_summary_errors_msg">Erros</string>
|
||||
<string name="sensor_state_cadence_avg">Cadencia media</string>
|
||||
<string name="sensor_state_cadence_max">Cadencia máx.</string>
|
||||
<string name="sensor_state_heart_rate_avg">Pulso medio</string>
|
||||
<string name="sensor_state_heart_rate_max">Pulso máximo</string>
|
||||
<string name="sensor_state_power_avg">Potencia media</string>
|
||||
</resources>
|
||||
@@ -323,11 +323,9 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="settings_stats_units_title">Unità di misura preferite</string>
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file">Condividi file di percorso</string>
|
||||
<string name="share_track_share_file_body">Credo che ti potrebbe interessare questo percorso.
|
||||
<string name="share_track_share_file_body">Penso che potrebbe essere interessato a questa pista.
|
||||
\n
|
||||
\n%1$s
|
||||
\n
|
||||
\nPer visualizzarlo, usa Google Earth o un altro software di mappatura.</string>
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">Desidero condividere un percorso con te</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">Ritmo medio in movimento</string>
|
||||
@@ -583,7 +581,6 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="settings_night_mode_option_night">Scuro</string>
|
||||
<string name="settings_sensor_connecting">Connesso a %1$s</string>
|
||||
<string name="settings_sensor_disconnected">Disconnesso da %1$s</string>
|
||||
<string name="settings_sensor_wheel_circumference">Grandezza della ruota (mm)</string>
|
||||
<string name="track_detail_intervals_tab">Intervalli</string>
|
||||
<string name="track_detail_record_success">Sto registrando un nuovo percorso</string>
|
||||
<string name="track_detail_resume_success">Riprendendo il percorso</string>
|
||||
@@ -594,4 +591,16 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="hold_to_pause">Tieni premuto per mettere in pausa</string>
|
||||
<string name="instant_export_enabled_summary">È stata completata l\' esportazione del percorso finito nella memoria interna del tuo dispositivo</string>
|
||||
<string name="instant_export_enabled_title">Esportazione istantanea post-allenamento</string>
|
||||
<string name="generic_open_track">Apri traccia</string>
|
||||
<string name="import_prevent_reimport">È stato impostato impedisci reimportazione traccia</string>
|
||||
<string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la directory di esportazione.</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Impedire la reimportazione delle tracce</string>
|
||||
<string name="export_progress_summary_errors_msg">Errore</string>
|
||||
<string name="export_progress_summary_skip_msg">Tracce saltate</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Tracce sovrascritte</string>
|
||||
<string name="export_progress_summary_new_msg">Nuove tracce esportate</string>
|
||||
<string name="import_no_kml_file_found">Nessun dottore.file kml trovato in KMZ</string>
|
||||
<string name="import_progress_summary_ok_msg">File importati</string>
|
||||
<string name="import_progress_summary_exists_msg">Esiste già</string>
|
||||
<string name="import_progress_summary_errors_msg">Errore</string>
|
||||
</resources>
|
||||
@@ -495,7 +495,6 @@ limitations under the License.
|
||||
<string name="settings_night_mode_option_system">System</string>
|
||||
<string name="settings_night_mode_option_day">Dag</string>
|
||||
<string name="settings_night_mode_option_night">Natt</string>
|
||||
<string name="settings_sensor_wheel_circumference">Hjulstørrelse (mm)</string>
|
||||
<string name="settings_error_initial_values_restored">Beklager, men det oppstod en feil. Alle innstillinger er tilbakestilt til standardverdiene.</string>
|
||||
<string name="stats_intervals">Intervaller</string>
|
||||
<string name="track_recording_notification_accuracy">Posisjonsnøyaktighet: %1$s</string>
|
||||
@@ -598,4 +597,10 @@ limitations under the License.
|
||||
<string name="generic_open_track">Åpne spor</string>
|
||||
<string name="import_progress_summary_exists_msg">Finnes allerede</string>
|
||||
<string name="import_progress_summary_errors_msg">Feil</string>
|
||||
<string name="sensor_state_cadence_avg">Gjennomsnittlig kadens</string>
|
||||
<string name="sensor_state_cadence_max">Maks kadens</string>
|
||||
<string name="sensor_state_heart_rate_avg">Gjennomsnittlig puls</string>
|
||||
<string name="sensor_state_heart_rate_max">Maks puls</string>
|
||||
<string name="sensor_state_power_avg">Gjennomsnittlig effekt</string>
|
||||
<string name="settings_sensor_wheel_circumference">Hjulomkrets (mm)</string>
|
||||
</resources>
|
||||
@@ -60,7 +60,7 @@
|
||||
<string name="activity_type_trail_hiking">caminhada em trilha</string>
|
||||
<string name="activity_type_trail_running">corrida em trilha</string>
|
||||
<string name="activity_type_train">treino</string>
|
||||
<string name="activity_type_ultimate_frisbee">ultimate frisbee</string>
|
||||
<string name="activity_type_ultimate_frisbee">frisbee definitivo</string>
|
||||
<string name="activity_type_unknown">desconhecida</string>
|
||||
<string name="activity_type_wakeboarding">esqui aquático</string>
|
||||
<string name="activity_type_walking">caminhada</string>
|
||||
@@ -233,7 +233,7 @@
|
||||
<string name="settings_reset_confirm_message">Todas as configurações serão revertidas para os valores padrão.</string>
|
||||
<string name="settings_reset_confirm_title">Redefinir agora?</string>
|
||||
<string name="settings_reset_done">Todas as configurações foram revertidas para os valores padrão</string>
|
||||
<string name="settings_sensor">Sensor</string>
|
||||
<string name="settings_sensor">Sensores</string>
|
||||
<string name="settings_sensor_bluetooth">Bluetooth</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor Bluetooth</string>
|
||||
<string name="settings_stats_coordinate">Latitude/longitude</string>
|
||||
@@ -526,7 +526,6 @@
|
||||
<string name="gps_disabled_msg">GPS desativado</string>
|
||||
<string name="gps_fixed_and_ready">GPS corrigido e pronto</string>
|
||||
<string name="settings_sensor_connecting">Conectado a %1$s</string>
|
||||
<string name="settings_sensor_wheel_circumference">Tamanho da roda (mm)</string>
|
||||
<string name="aggregated_stats_empty_message">Registre sua primeira trilha para ver as estatísticas agregadas</string>
|
||||
<string name="voice_pace_lap">Tempo da volta de %1$s</string>
|
||||
<string name="share_image_subject">Eu gostaria de compartilhar uma imagem com você</string>
|
||||
@@ -549,4 +548,10 @@
|
||||
<string name="import_progress_summary_ok_msg">Arquivos importados</string>
|
||||
<string name="import_progress_summary_exists_msg">Já existe</string>
|
||||
<string name="import_progress_summary_errors_msg">Erros</string>
|
||||
<string name="sensor_state_heart_rate_max">Freqüência cardíaca máxima</string>
|
||||
<string name="sensor_state_power_avg">Energia Avg</string>
|
||||
<string name="sensor_state_heart_rate_avg">Ritmo cardíaco Avg</string>
|
||||
<string name="sensor_state_cadence_max">Ritmo cardíaco médio</string>
|
||||
<string name="sensor_state_cadence_avg">Avg Cadência</string>
|
||||
<string name="settings_sensor_wheel_circumference">Circunferência da roda (mm)</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2010 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
--><resources>
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">Авиаспорт</string>
|
||||
<string name="activity_type_atv">Езда на внедорожниках</string>
|
||||
@@ -276,7 +276,6 @@ limitations under the License.
|
||||
<string name="settings_stats_units_imperial">Британская (мили, футы)</string>
|
||||
<string name="settings_stats_units_metric">Метрическая (км, м)</string>
|
||||
<string name="settings_stats_units_title">Система измерений</string>
|
||||
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file">Опубликовать трек</string>
|
||||
<string name="share_track_share_file_body">Я думаю этот маршрут покажется вам интересным.\n\nИспользуйте Google Планета Земля или другое картографическое ПО для просмотра трека.\n\n%1$s</string>
|
||||
@@ -461,4 +460,30 @@ limitations under the License.
|
||||
<string name="waypoint_type_waterfall">Водопад</string>
|
||||
<string name="waypoint_type_water_fountain">Фонтан</string>
|
||||
<string name="waypoint_type_waypoint">путевая точка</string>
|
||||
</resources>
|
||||
<string name="help_track_map_content"> Используйте значок маленькой карты.
|
||||
\na) (живые обновления) Установите <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Он предоставляет карты в режиме онлайн или оффлайн с помощью <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
\nНа самом деле, OSMDashboard - это отдельное приложение, которое может получить доступ (временно) только к данным треков, которые будут показаны.
|
||||
\nb) Через обычное картографическое приложение (например, <a href="http://osmand.net/">OsmAnd</a> и <a href="https://maps.me/">MAPS.ME</a>). Эти приложения должны поддерживать формат файлов KMZ (только местоположения и метки времени). Фактически, приложение должно поддерживать <i>ACTION_VIEW</i> с MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||
<string name="help_storage_pictures_title">Как OpenTracks хранит изображения, сделанные в качестве маркеров\?</string>
|
||||
<string name="about_preference_title">О OpenTracks</string>
|
||||
<string name="about_description"><i>OpenTracks</i> - это приложение для отслеживания спортивных событий, которое полностью уважает вашу конфиденциальность.</string>
|
||||
<string name="about_url">Веб-страница проекта: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Название версии: %1$s</string>
|
||||
<string name="about_version_code">Код версии: %1$d</string>
|
||||
<string name="about_history">OpenTracks - это форк <a href="https://code.google.com/archive/p/mytracks/"><i>Google MyTracks</i></a> (разработка прекращена в 2016 г.).</string>
|
||||
<string name="about_libraries">Библиотеки</string>
|
||||
<string name="help_feature_title">OpenTracks имеет следующие возможности:</string>
|
||||
<string name="help_feature_content">* создание GPS-треков
|
||||
\n* использование датчика частоты пульса (только Bluetooth LE)
|
||||
\n* голосовые объявления (по времени или расстоянию)
|
||||
\n* создание маркеров (в т.ч. картинки)
|
||||
\n* работа полностью офлайн
|
||||
\n* показ треков на карте (см. ниже)</string>
|
||||
<string name="help_track_map_title">Как показать трек на карте\?</string>
|
||||
<string name="help_storage_pictures_content">Они хранятся во внутренней памяти приложения (т. е. доступны только в OpenTracks).</string>
|
||||
<string name="help_track_export_title">Как я могу экспортировать/поделиться записанными треками\?</string>
|
||||
<string name="help_data_google_play_services_title">Требуется/используется ли OpenTracks сервисы Google Play\?</string>
|
||||
<string name="help_recording_title">Как OpenTracks записывает данные\?</string>
|
||||
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> предоставляется как KMZ. </string>
|
||||
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2010 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
--><resources>
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">坐飞机</string>
|
||||
<string name="activity_type_atv">全地形越野</string>
|
||||
@@ -118,7 +118,7 @@ limitations under the License.
|
||||
<string name="description_track_point">点</string>
|
||||
<string name="description_track_segment">路段</string>
|
||||
<!-- Export -->
|
||||
<string name="export_all_option">全部以 %1$s 格式导</string>
|
||||
<string name="export_all_option">以 %1$s 格式导出全部</string>
|
||||
<string name="export_all_title">导出到外部存储设备</string>
|
||||
<string name="export_error">已将%1$d条路线(共%2$s条)导出到%3$s</string>
|
||||
<string name="export_option">以 %1$s 格式导出到 %2$s</string>
|
||||
@@ -278,11 +278,11 @@ limitations under the License.
|
||||
<string name="settings_stats_units_imperial">英制(英里、英尺)</string>
|
||||
<string name="settings_stats_units_metric">公制(公里、米)</string>
|
||||
<string name="settings_stats_units_title">首选单位</string>
|
||||
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file">分享路线文件</string>
|
||||
<string name="share_track_share_file_body">您可能会对此路线感兴趣。\n\n要查看此路线,请使用 Google 地图或其他
|
||||
地图软件。\n\n%1$s</string>
|
||||
<string name="share_track_share_file_body">我想您可能会对此路线感兴趣。
|
||||
\n
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">我要与您分享路线</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">平均移动步速</string>
|
||||
@@ -464,4 +464,18 @@ limitations under the License.
|
||||
<string name="waypoint_type_waterfall">瀑布</string>
|
||||
<string name="waypoint_type_water_fountain">喷泉</string>
|
||||
<string name="waypoint_type_waypoint">路标</string>
|
||||
</resources>
|
||||
<string name="about_url">项目主页:<a href="%1$s">%1$s</a></string>
|
||||
<string name="about_history">OpenTracks 是 <a href="https://code.google.com/archive/p/mytracks/"><i>Google MyTracks</i></a>(终止于 2016 年)的一个分支版本。</string>
|
||||
<string name="open_track_as_KMZ">作为 KMZ 文件打开</string>
|
||||
<string name="open_track_as_trackfileformat">作为 %1$s 文件打开</string>
|
||||
<string name="about_preference_title">关于 OpenTracks</string>
|
||||
<string name="about_version_name">版本名称:%1$s</string>
|
||||
<string name="about_version_code">版本号:%1$d</string>
|
||||
<string name="about_libraries">库</string>
|
||||
<string name="help_feature_title">OpenTracks 支持以下特性:</string>
|
||||
<string name="open_track_as_GPX">作为 GPX 文件打开</string>
|
||||
<string name="activity_type_escooter">电动车</string>
|
||||
<string name="app_not_installed_show_on_map">请先安装一个支持展示地图数据的应用。</string>
|
||||
<string name="description_elevation_loss">海拔降低:%1$d 米(%2$d 英尺)</string>
|
||||
<string name="description_default_speed_or_pace">按活动类型</string>
|
||||
</resources>
|
||||
@@ -360,11 +360,16 @@ limitations under the License.
|
||||
<!-- Sensor State -->
|
||||
<string name="sensor_state_battery">Battery level</string>
|
||||
<string name="sensor_state_cadence">Cadence</string>
|
||||
<string name="sensor_state_cadence_avg">Avg Cadence</string>
|
||||
<string name="sensor_state_cadence_max">Max Cadence</string>
|
||||
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
||||
<string name="sensor_state_disconnected">Disconnected</string>
|
||||
<string name="sensor_state_heart_rate">Heart rate</string>
|
||||
<string name="sensor_state_heart_rate_avg">Avg Heart rate</string>
|
||||
<string name="sensor_state_heart_rate_max">Max Heart rate</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
|
||||
<string name="sensor_state_power">Power</string>
|
||||
<string name="sensor_state_power_avg">Avg Power</string>
|
||||
<string name="sensor_state_power_value">%1$d W</string>
|
||||
<string name="sensor_not_known">Sensor %1$s is unknown. Please check the settings.</string>
|
||||
<string name="sensor_could_not_scan">Could not scan for Bluetooth devices (error %1$i).</string>
|
||||
@@ -420,7 +425,7 @@ limitations under the License.
|
||||
<string name="settings_sensor_connecting">Connected to %1$s</string>
|
||||
<string name="settings_sensor_disconnected">Disconnected from %1$s</string>
|
||||
|
||||
<string name="settings_sensor_wheel_circumference">Wheel size (mm)</string>
|
||||
<string name="settings_sensor_wheel_circumference">Wheel circumference (mm)</string>
|
||||
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_coordinate">Latitude/Longitude</string>
|
||||
|
||||
Reference in New Issue
Block a user