Compare commits

..

37 Commits

Author SHA1 Message Date
Dennis Guse 14e63a1a54 Release: v3.15.4 2021-03-09 21:44:02 +01:00
Román Martínez e559583d23 Voice announcement in seconds instead of minutes. Fixes #640. 2021-03-09 20:14:54 +01:00
Dennis Guse 3b9711d82e Release: v3.15.3 2021-03-08 18:16:18 +01:00
Dennis Guse 114fc8255f KML export: remove #start (was broken due to #316) and #end (included track statistics) placemarks.
Fixes #629.
2021-03-08 18:11:36 +01:00
Dennis Guse 26e655d58f Cleanup. 2021-03-08 18:11:36 +01:00
Dennis Guse 815b6a3cf1 StatisticsRecordingFragment: set speed=0 when TrackPoint is outdated. 2021-03-07 22:28:53 +01:00
Dennis Guse 3dc8a724d8 Bug: StatisticsRecordingFragment: speed and location where sometimes 0.
Fixes #628.
2021-03-07 22:28:53 +01:00
Dennis Guse 70f571cd09 CI: avoid OutOfMemoryError.
Fixes #599.
2021-03-07 19:46:37 +01:00
Dennis Guse 3f5c3a5c08 Bugfix: Close cursor. 2021-03-06 18:27:24 +01:00
Dennis Guse a13465391f Import: replaced SAXParserException as it only shows the message of it's cause. 2021-03-06 18:13:06 +01:00
Dennis Guse 97454b5369 Statistics*Fragment now updates the units on track category change. 2021-03-04 19:18:31 +01:00
Dennis Guse f6a47c4594 Statistics*Fragement now uses SharedPreferenceListener instead of accessing preferences repeatedly. 2021-03-04 19:18:31 +01:00
Dennis Guse fce76230e1 TrackDataListener now passes most recent TrackPoint to onNewTrackPointsDone(). 2021-03-04 19:18:31 +01:00
Dennis Guse fdf4336a9b Cleanup. 2021-03-04 19:18:31 +01:00
Dennis Guse 844c6e9609 TrackPoint: moved methods into class. 2021-03-04 19:18:31 +01:00
Manuel Zangl 784942ffd2 Fixed longitudeText in StatisticsRecordingFragment 2021-03-04 17:38:42 +01:00
Román Martínez edf4ccc941 It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence. 2021-03-02 19:55:18 +01:00
Dennis Guse d90258294f Release: v3.15.2 2021-03-01 19:42:16 +01:00
mondstern 01f4c13307 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-02-28 20:56:11 +01:00
FTno 873956426d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
Xosé M 4128c54960 Translated using Weblate (Galician)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-02-28 20:56:11 +01:00
mondstern 9978bddf21 Translated using Weblate (Czech)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Nikita Epifanov a9726b9a94 Translated using Weblate (Russian)
Currently translated at 81.5% (429 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-02-28 20:56:11 +01:00
Crystal-RainSlide 05ba52bab5 Translated using Weblate (Chinese (Simplified))
Currently translated at 80.7% (425 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/zh_Hans/
2021-02-28 20:56:11 +01:00
FTno ca95f40e77 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
mondstern 5cb85c4383 Translated using Weblate (Italian)
Currently translated at 98.4% (513 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-02-28 20:56:11 +01:00
mondstern d0a477b112 Translated using Weblate (Finnish)
Currently translated at 94.4% (492 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-02-28 20:56:11 +01:00
mondstern ec365a18ca Translated using Weblate (Danish)
Currently translated at 88.2% (460 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-02-28 20:56:11 +01:00
mondstern 0ff735e559 Translated using Weblate (Czech)
Currently translated at 100.0% (521 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Dennis Guse 8a35835791 GPX export: re-implemented basic support for multiple tracks.
Fixes #614.
2021-02-28 20:56:05 +01:00
Dennis Guse e3569dab56 Release: v3.15.1 2021-02-27 18:40:05 +01:00
Dennis Guse 4040e3f45b Bugfix: crash on recording start due to show latitude/longitude option.
Fixes #613.
2021-02-27 18:37:42 +01:00
Dennis Guse eac7efba64 Upgrade desugar lib. 2021-02-26 21:30:08 +01:00
dependabot[bot] 21a3971a56 Bump mockito-android from 3.7.7 to 3.8.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.7.7 to 3.8.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.7.7...v3.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-23 08:07:43 +01:00
Dennis Guse 4e4a9abf6f Localization: fix/remove translation for wheel circumference (instead of wheel size).
Fixes #605.
2021-02-22 23:11:06 +01:00
Román Martínez a9624f72e7 Track recorded stats shows sensor data from trackpoints table.
Heart rate: maximum and average.
Cadence: maximum and average.
Power: average.
Fixes #525 and #502.
2021-02-14 17:34:19 +01:00
Dennis Guse bb38d274fb Voice announcements: cleanup. 2021-02-13 14:27:54 +01:00
59 changed files with 1323 additions and 650 deletions
+2
View File
@@ -18,4 +18,6 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedCheck
+4 -4
View File
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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>
+15 -1
View File
@@ -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>
+8 -25
View File
@@ -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 &lt;a href=\"https://github.com/OpenTracksApp/OSMDashboard\"&gt; OSMDashboard &lt;/a&gt;. Det giver online eller offline kort via &lt;a href=\"https://www.openstreetmap.org\"&gt; OpenStreetMap &lt;/a&gt;.
\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. &lt;a href=\"http://osmand.net/\"&gt; OsmAnd &lt;/a&gt; og &lt;a href=\"https://maps.me/\"&gt; MAPS.ME &lt; / a&gt;). Disse apps skal understøtte KMZ-filformatet (kun placeringer og tidsstempler). Faktisk skal en app understøtte &lt;i&gt; ACTION_VIEW &lt;/i&gt; med MIME &lt;i&gt; -applikation / vnd.google-earth.kmz &lt;/i&gt;. "</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>
+48 -41
View File
@@ -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 &lt;a href=\"https://github.com/OpenTracksApp/OSMDashboard\"&gt;OSMDashboard&lt;/a&gt;. Se tarjoaa online-tai offline-karttoja kautta &lt;a href=\"https://www.openstreetmap.org\"&gt;OpenStreetMap&lt;/a&gt;.
\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. &lt;a href=\"http://osmand.net/\"&gt;OsmAnd&lt;/a&gt; ja &lt;a href=\"https://maps.me/\"&gt;KARTAT.MINULLE&lt;/a&gt;). Nämä sovellukset tarvitsevat tukea KMZ-muodossa (vain paikoissa ja aikaleimat). Itse asiassa sovelluksen on tuettava &lt; i&gt;ACTION_VIEW&lt; / i&gt; MIME &lt;i&gt;application / vnd: n kanssa.google-maa.kmz&lt;/i&gt;. "</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>
+6 -1
View File
@@ -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>
+14 -5
View File
@@ -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>
+6 -1
View File
@@ -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>
+8 -3
View File
@@ -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>
+30 -5
View File
@@ -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>
+22 -8
View File
@@ -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>
+6 -1
View File
@@ -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>