Compare commits

...

23 Commits

Author SHA1 Message Date
Dennis Guse 9affe28fad Release: v4.25.0 2026-01-21 18:33:40 +01:00
Dennis Guse 4905a37951 Build scripts: add some error handling. 2026-01-21 18:33:20 +01:00
Dennis Guse 439671d0a4 Build scripts: add some error handling. 2026-01-18 15:49:41 +01:00
Dennis Guse 825e8dda6e Revert "Translated using Weblate (English (en_001))"
This reverts commit 99a9248b27.
2026-01-18 10:13:00 +01:00
Dennis Guse 661c839c78 Update README_TESTED_SENSORS.md
Assioma Pro MX-2

Fixes #2246.
2026-01-18 10:08:42 +01:00
Dennis Guse 8eb42a7ecb Upgrade to Android Gradle Plugin 9.0: removing applicationVariants 2026-01-17 22:58:17 +01:00
CI/CD Bot by pstorch 660a55ff5a Update dependency gradle to v9.3.0 2026-01-16 12:45:31 +00:00
CI/CD Bot by pstorch 958697bcb3 Update dependency com.android.tools.build:gradle to v9 2026-01-15 17:42:07 +00:00
ntd 6029eb0317 Translated using Weblate (Italian)
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2026-01-10 21:48:12 +01:00
ntd 8b56d53a6a Translated using Weblate (Italian)
Currently translated at 3.9% (7 of 178 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2026-01-10 21:48:12 +01:00
Dennis Guse e9deebea6c Refactor: RecordingLayout. 2026-01-10 21:43:54 +01:00
Dennis Guse 54844b9a9a Sensor: add temperature.
Fixes #2241.
2026-01-10 21:43:54 +01:00
Dennis Guse 738201c1c2 Cleanup. 2026-01-04 09:57:05 +01:00
discollizard 48b6be0bd1 record button moved to the middle of the screen
Part of #2199
2026-01-03 18:40:17 -03:00
Priit Jõerüüt 6b924cd936 Translated using Weblate (Estonian)
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-01-02 19:56:27 +01:00
discollizard 5f083de530 adjusted distance time and markers to be horizontally consistent
formatter

initial date and time are always visible in the list while recording
2026-01-02 15:31:42 -03:00
Matt Votava 278f6323f1 track import: get extension from file name, not URI
When importing tracks from nextcloud, the URI for a file may be something like

content://org.nextcloud.documents/tree/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F707/document/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F745

The URI contains no ".", so getExtension() would return the whole URI as the extension, resulting in an "Unsupported file format" error.

To fix this, also pass DocumentFile.getName() to the ImportWorker.
2025-12-28 01:30:10 -08:00
Dennis Guse 5baa8fa21e Cleanup: KMLTrackImporter doesn't need Position at first. 2025-12-28 08:59:19 +01:00
Dennis Guse 707f4add71 Cleanup: GpsStatusManager only needs Position. 2025-12-28 08:59:19 +01:00
Dennis Guse b583f8f23d Sticker: added printable PDF 2025-12-27 08:38:01 +01:00
Priit Jõerüüt c847f622e4 Translated using Weblate (Estonian)
Currently translated at 91.3% (495 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2025-12-27 07:35:12 +01:00
Priit Jõerüüt 70dd58f484 Translated using Weblate (Estonian)
Currently translated at 78.9% (428 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2025-12-27 07:35:12 +01:00
Dennis Guse 015352c989 TrackPoint is now a record. 2025-12-24 17:15:19 +01:00
101 changed files with 2845 additions and 1868 deletions
+4 -1
View File
@@ -44,7 +44,10 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
For cycling (incl. cadence):
* Assioma Duo Powermeter
* Assioma
* Duo Powermeter
* Pro MX-2
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
+2 -5
View File
@@ -11,9 +11,6 @@ CHANGELOG_FILE="$CHANGELOG_DIR/$VERSIONID.txt"
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
#TODO Execute all tests: we are destroying data, running on a real device.
# ./gradlew connectedCheck
# Changelog
echo -e "\nPrevious changelog\n------------"
PREVIOUS_CHANGELOG=$(find "$CHANGELOG_DIR" -iname "[[:digit:]]**.txt" | sort -n | tail -1)
@@ -34,8 +31,8 @@ gedit -w "$CHANGELOG_FILE"
git add "$CHANGELOG_FILE"
echo "Updating build.gradle"
sed -i s/versionCode\ .*/versionCode\ "$VERSIONID"/g build.gradle
sed -i s/versionName\ .*/versionName\ "\"$VERSIONNAME\""/g build.gradle
sed -i -e "s/versionCode\ [0-9]\{1,\}/versionCode\ $VERSIONID/g" build.gradle
sed -i -e "s/versionName\ \"v[0-9\.]\{1,\}\"/versionName\ \"$VERSIONNAME\"/g" build.gradle
git diff
+7 -2
View File
@@ -6,8 +6,13 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64/
JAVA_VERSION=`java --version`
if [[ $JAVA_VERSION != "openjdk 21."* ]]; then
echo "OpenJDK version should be 21.X"
exit -1;
echo -e "OpenJDK version should be 21.X"
fi
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
if [[ $(git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline | wc -l) -gt 0 ]]; then
echo -e "No version tag for this commit; please use RELEASE.sh";
fi
RELEASE_STORE_PASSWORD=
+15 -20
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.2'
classpath 'com.android.tools.build:gradle:9.0.0'
}
}
@@ -38,6 +38,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
androidResources {
@@ -46,8 +47,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6516
versionName "v4.24.2"
versionCode 6539
versionName "v4.25.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -106,12 +107,19 @@ android {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.nightly'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
signingConfig = signingConfigs.nightly
versionCode getVersionCode()
versionName getVersionName()
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCode()}"
}
irreproducible {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
dimension 'version'
applicationId 'de.dennisguse.opentracks'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
versionNameSuffix 'irreproducible'
}
reproducible {
@@ -120,27 +128,14 @@ android {
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
dimension 'version'
applicationId 'de.dennisguse.opentracks.playstore'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${getVersionCode()}"
signingConfig = signingConfigs.release
}
}
applicationVariants.configureEach { variant ->
variant.resValue 'string', 'applicationId', variant.applicationId
variant.outputs.configureEach {
if (variant.flavorName == 'nightly') {
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
}
if (variant.flavorName == 'reproducible') {
outputFileName = "${applicationId}_${variant.versionName}.apk"
} else {
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -0,0 +1,13 @@
v4.25.0: OpenTracks
Changes:
- Record button is centered
- Support for BLE thermometer (Environmental Sensing)
Bugfix:
- On import: get type from filename (not URI)
Developer:
- TrackPoint is a record
- Update to Android Gradle Plugin 9.0
@@ -4,7 +4,7 @@ Modifiche:
- Velocità degli annunci vocali configurabile
- API <26: L'icona del lanciatore omette lo sfondo
Bugfix:
Correzioni:
- UI di registrazione: mostra i valori corretti
- Registrazione UI: mostra (di nuovo) l'altitudine in EGM2008
@@ -1,4 +1,4 @@
v4.22.1: OpenTracks
Modiciche:
Modifiche:
- Primo rilascio da Codeberg
@@ -1,4 +1,4 @@
v4.22.2: OpenTracks
Correzione di errori:
Correzioni:
- L'impostazione degli annunci vocali per l'ora e l'unità di misura è stata collegata
@@ -3,7 +3,7 @@
Modifiche:
-
Correzioni di errori:
Correzioni:
-
Sviluppatore:
@@ -1 +1 @@
Un tracker sportivo che rispetta completamente la tua privacy.
Un tracker sportivo che rispetta la tua privacy.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Binary file not shown.
@@ -17,9 +17,13 @@ import de.dennisguse.opentracks.data.MarkerIterator;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -92,9 +96,9 @@ public class TestDataUtil {
);
List<Marker> markers = List.of(
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).getPosition(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).getPosition(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).getPosition(), null)
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).position(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).position(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).position(), null)
);
return new TrackData(track, trackPoints, markers);
@@ -112,8 +116,10 @@ public class TestDataUtil {
return pair.first;
}
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT,
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type, AltitudeGainLoss altitudeGainLoss) {
return new TrackPoint(
null,
type,
new Position(
Instant.ofEpochSecond(i),
INITIAL_LATITUDE + (double) i / 10000.0,
@@ -123,21 +129,25 @@ public class TestDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
));
trackPoint.setHeartRate(100f + i % 80);
trackPoint.setCadence(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setAltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS);
return trackPoint;
),
null,
HeartRate.of(100f + i % 80),
null,
Cadence.of(300f + i),
Power.of(400f + i),
altitudeGainLoss
);
}
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
TrackPoint trackPoint = createTrackPoint(i);
trackPoint.setType(type);
return trackPoint;
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT, new AltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS));
}
public static TrackPoint createTrackPoint(int i) {
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT);
}
/**
* Inserts a track with locations into the database.
*
@@ -164,7 +174,7 @@ public class TestDataUtil {
"Marker name",
description,
"Marker typeLocalized",
trackPoint.getPosition(),
trackPoint.position(),
photoUri);
}
@@ -6,8 +6,12 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -20,7 +24,9 @@ public class TestSensorDataUtil {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
int i = trackPointList.size() + 1;
TrackPoint tp = new TrackPoint(type,
TrackPoint tp = new TrackPoint(
null,
type,
new Position(
time,
TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0,
@@ -30,18 +36,15 @@ public class TestSensorDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
)
),
null,
HeartRate.ofOrNull(hr),
null,
cadence != null ? Cadence.of(cadence) : null,
power != null ? Power.of(power) : null,
new AltitudeGainLoss(3, 3)
);
if (hr != null) {
tp.setHeartRate(hr);
}
if (cadence != null) {
tp.setCadence(cadence);
}
if (power != null) {
tp.setPower(power);
}
tp.setAltitudeGainLoss(3, 3);
trackPointList.add(tp);
}
@@ -54,19 +54,22 @@ import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.MarkerBuilder;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -124,7 +127,7 @@ public class CustomContentProviderUtilsTest {
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.getId().id());
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.id().id());
}
assertEquals(numPoints, locations.size());
}
@@ -167,7 +170,7 @@ public class CustomContentProviderUtilsTest {
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(id, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId();
lastPointId = trackPoint.id();
counter++;
}
}
@@ -237,7 +240,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -300,7 +303,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId1).getPosition(),
getLastValidTrackPoint(trackId1).position(),
null
));
@@ -473,7 +476,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -553,7 +556,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
Marker.Id marker2Id = contentProviderUtils.insertMarker(new Marker(
@@ -562,7 +565,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -589,7 +592,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
}
@@ -613,7 +616,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -636,7 +639,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -647,7 +650,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC_NEW,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -803,20 +806,27 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = TestDataUtil.createTrackPoint(5);
trackPoint.setHeartRate(1F);
trackPoint.setCadence(2F);
trackPoint.setPower(3F);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(5).position(),
null,
HeartRate.of(1),
null,
Cadence.of(2),
Power.of(3),
null
);
// when
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrue(trackPoints.get(10).hasHeartRate());
assertEquals(trackPoint.getHeartRate(), trackPoints.get(10).getHeartRate());
assertEquals(trackPoint.getCadence(), trackPoints.get(10).getCadence());
assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower());
assertNotNull(trackPoints.get(10).heartRate());
assertEquals(trackPoint.heartRate(), trackPoints.get(10).heartRate());
assertEquals(trackPoint.cadence(), trackPoints.get(10).cadence());
assertEquals(trackPoint.power(), trackPoints.get(10).power());
}
@Test
@@ -857,9 +867,9 @@ public class CustomContentProviderUtilsTest {
for (int i = 0; i < trackpointIds.size(); i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId.id() + i, trackPoint.getId().id());
assertEquals(startTrackPointId.id() + i, trackPoint.id().id());
Position position = trackPoint.getPosition();
Position position = trackPoint.position();
assertEquals(TestDataUtil.INITIAL_LATITUDE + i / 10000.0, position.latitude(), 0.01);
assertEquals(TestDataUtil.INITIAL_LONGITUDE - i / 10000.0, position.longitude(), 0.01);
assertEquals(i / 100.0, position.horizontalAccuracy().distance_m(), 0.01);
@@ -879,11 +889,18 @@ public class CustomContentProviderUtilsTest {
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCadence(null);
trackPoint.setHeartRate(null);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(1).position(),
null,
null,
null,
null,
null,
null
);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
@@ -1217,7 +1234,7 @@ public class CustomContentProviderUtilsTest {
return trackpoints.reversed()
.stream()
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.getType()))
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.type()))
.findFirst().orElseThrow();
}
@@ -13,7 +13,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointTest {
@Test
public void distanceToPrevious() {
public void distanceToPrevious_gps() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
@@ -38,20 +38,123 @@ public class TrackPointTest {
// without sensor distance
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_first() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null));
// tp1 has sensor distance
tp1.setSensorDistance(Distance.of(5));
tp1.setSensorDistance(null);
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_second() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null));
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp2 has sensor distance
tp1.setSensorDistance(null);
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_both() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(10),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp1 and tp2 have sensor distance
tp1.setSensorDistance(Distance.of(10));
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
}
@@ -26,7 +26,6 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.BufferedReader;
import java.io.File;
@@ -61,20 +60,22 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@@ -123,11 +124,13 @@ public class ExportImportTest {
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
public void fileSetup() throws IOException, TimeoutException {
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(200), true);
setUp();
}
@After
@@ -158,20 +161,28 @@ public class ExportImportTest {
"Marker 1",
"Marker 1 desc",
"Marker 1 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567,
Distance.of(10),
Altitude.WGS84.of(1020.25),
Distance.of(13),
null,
Speed.of(15)
),
null
));
// A sensor-only TrackPoint
trackPointCreator.setClock("2020-02-02T02:02:04Z");
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f);
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f, 27);
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null);
trackPointCreator.setClock("2020-02-02T02:02:14Z"); //ignored
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null, 28);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null);
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null, 29);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null, 30); //Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
contentProviderUtils.insertMarker(
@@ -181,12 +192,12 @@ public class ExportImportTest {
"Marker 2",
"Marker 2 desc",
"Marker 2 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
service.getLastStoredTrackPointWithLocation().position(),
null
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -201,7 +212,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -218,13 +229,134 @@ public class ExportImportTest {
@LargeTest
@Test
public void track() throws TimeoutException {
setUp();
Track track = contentProviderUtils.getTrack(trackId);
Statistics trackStatistics = track.statistics();
assertEquals(ZoneOffset.of("+01:00"), track.zoneOffset());
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:04Z"),
null, null, null,
null, null,
null,
Speed.of(15)),
Distance.of(10),
HeartRate.of(66),
Temperature.of(27),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse("2020-02-02T02:02:15Z")),
null,
HeartRate.of(68),
Temperature.of(29),
Cadence.of(3),
Power.of(50),
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)),
Distance.of(2),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.parse("2020-02-02T02:03:30Z")),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
assertEquals(new Statistics(
Instant.parse("2020-02-02T02:02:02Z"),
Instant.parse("2020-02-02T02:04:00Z"),
@@ -238,77 +370,12 @@ public class ExportImportTest {
null
),
trackStatistics);
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Instant.parse("2020-02-02T02:02:04Z"))
.setSensorDistance(Distance.of(10))
.setSpeed(Speed.of(15))
.setHeartRate(HeartRate.of(66))
.setCadence(3)
.setPower(50).setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:15Z"))
.setHeartRate(HeartRate.of(68))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(2))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(69))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
//TODO Does not test marker images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp();
public void kmz_with_trackdetail_and_sensordata() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -360,9 +427,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -387,8 +452,6 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException, IOException {
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -421,51 +484,86 @@ public class ExportImportTest {
.setDelta(0.05); // speed is not fully
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456d, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
// 3. trackstatistics
@@ -492,9 +590,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void gpx_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -520,9 +616,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void csv_export_only() throws TimeoutException, IOException {
setUp();
public void csv_export_only() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -566,17 +660,17 @@ public class ExportImportTest {
}
}
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
AggregatorCyclingPower cyclingPower = new AggregatorCyclingPower("", "");
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerManagerCyclingPower.Data(Power.of(power), null)));
sensorDataSet.add(cyclingPower);
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerCyclingPower.Data(Power.of(power), null)));
sensorDataAggregator.add(cyclingPower);
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
avgHeartRate.add(new Raw<>(trackPointCreator.createNow(), HeartRate.of(heartRate)));
sensorDataSet.add(avgHeartRate);
sensorDataAggregator.add(avgHeartRate);
AggregatorCyclingCadence cyclingCadence = new AggregatorCyclingCadence("", "") {
@NonNull
@@ -590,33 +684,65 @@ public class ExportImportTest {
return true;
}
};
sensorDataSet.add(cyclingCadence);
sensorDataAggregator.add(cyclingCadence);
if (distance != null && speed != null) {
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasReceivedData()).thenReturn(true);
Mockito.when(distanceSpeed.getAggregatedValue(Mockito.any())).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
sensorDataSet.add(distanceSpeed);
AggregatorCyclingDistanceSpeed aggregatorCyclingDistanceSpeed = new AggregatorCyclingDistanceSpeed("", "") {
@NonNull
@Override
public Data getAggregatedValue(Instant now) {
return new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed));
}
@Override
public boolean hasReceivedData() {
return true;
}
};
sensorDataAggregator.add(aggregatorCyclingDistanceSpeed);
} else {
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
sensorDataAggregator.add(new AggregatorCyclingDistanceSpeed("", ""));
}
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.onChange(sensorDataSet);
sensorDataAggregator.add(new AggregatorTemperature("", "'") {
@NonNull
@Override
public Temperature getAggregatedValue(Instant now) {
return Temperature.of(temperature);
}
@Override
public boolean hasReceivedData() {
return true;
}
});
trackPointCreator.onChange();
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
if (altitudeGain != null) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasReceivedData()).thenReturn(true);
Mockito.when(barometer.getAggregatedValue(Mockito.any())).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer("test", null));
if (altitudeGain == null) {
sensorDataAggregator.add(new AggregatorBarometer("test", null));
return;
}
AggregatorBarometer aggregatorBarometer = new AggregatorBarometer("", "") {
@NonNull
@Override
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
@Override
public boolean hasReceivedData() {
return true;
}
};
sensorDataAggregator.add(aggregatorBarometer);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
@@ -27,8 +27,12 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -193,51 +197,86 @@ public class GPXTrackImporterTest {
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05); // speed is not fully
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3d, 14d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3d, 14.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
null,
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3d, 14.002, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3d, 16d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3d, 16.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
}
@@ -247,7 +286,7 @@ public class GPXTrackImporterTest {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
try(InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
@@ -16,57 +16,47 @@ public class TrackPointAssert {
public void assertEquals(TrackPoint expected, TrackPoint actual) {
Assert.assertEquals("time", expected.getTime(), actual.getTime());
Assert.assertEquals("type", expected.getType(), actual.getType());
Assert.assertEquals("type", expected.type(), actual.type());
Assert.assertEquals("has location,", expected.hasLocation(), actual.hasLocation());
if (expected.hasLocation()) {
Assert.assertEquals("latitude", expected.getPosition().latitude(), actual.getPosition().latitude(), 0.001);
Assert.assertEquals("longitude", expected.getPosition().longitude(), actual.getPosition().longitude(), 0.001);
Assert.assertEquals("has location,", expected.position().hasLocation(), actual.position().hasLocation());
if (expected.position().hasLocation()) {
Assert.assertEquals("latitude", expected.position().latitude(), actual.position().latitude(), 0.001);
Assert.assertEquals("longitude", expected.position().longitude(), actual.position().longitude(), 0.001);
}
Assert.assertEquals("has altitude", expected.hasAltitude(), actual.hasAltitude());
if (expected.hasAltitude()) {
Assert.assertEquals("altitude", expected.getAltitude().toM(), actual.getAltitude().toM(), delta);
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
if (expected.position().hasAltitude()) {
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
}
Assert.assertEquals("has altitudeGainLoss", expected.hasAltitudeGainLoss(), actual.hasAltitudeGainLoss());
if (expected.hasAltitudeGainLoss()) {
Assert.assertEquals("altitudeGainLoss", expected.getAltitudeGainLoss(), actual.getAltitudeGainLoss());
Assert.assertEquals("altitudeGainLoss", expected.altitudeGainLoss(), actual.altitudeGainLoss());
Assert.assertEquals("has speed", expected.position().hasSpeed(), actual.position().hasSpeed());
if (expected.position().hasSpeed()) {
Assert.assertEquals("speed", expected.position().speed().toMPS(), actual.position().speed().toMPS(), delta);
}
Assert.assertEquals("has speed", expected.hasSpeed(), actual.hasSpeed());
if (expected.hasSpeed()) {
Assert.assertEquals("speed", expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
Assert.assertEquals("has horizontalAccuracy", expected.position().hasHorizontalAccuracy(), actual.position().hasHorizontalAccuracy());
if (expected.position().hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.position().horizontalAccuracy().toM(), actual.position().horizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.position().hasVerticalAccuracy(), actual.position().hasVerticalAccuracy());
if (expected.position().hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.position().verticalAccuracy().toM(), actual.position().verticalAccuracy().toM(), delta);
}
Assert.assertEquals("has horizontalAccuracy", expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
if (expected.hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
Assert.assertEquals("has sensorDistance", expected.sensorDistance() != null, actual.sensorDistance() != null);
if (expected.sensorDistance() != null) {
Assert.assertEquals("sensorDistance", expected.sensorDistance().toM(), actual.sensorDistance().toM(), delta);
}
Assert.assertEquals("has sensorDistance", expected.hasSensorDistance(), actual.hasSensorDistance());
if (expected.hasSensorDistance()) {
Assert.assertEquals("sensorDistance", expected.getSensorDistance().toM(), actual.getSensorDistance().toM(), delta);
}
Assert.assertEquals("heartRate", expected.heartRate(), actual.heartRate());
Assert.assertEquals("has heartrate", expected.hasHeartRate(), actual.hasHeartRate());
if (expected.hasHeartRate()) {
Assert.assertEquals("heartrate", expected.getHeartRate(), actual.getHeartRate());
}
Assert.assertEquals("temperature", expected.temperature(), actual.temperature());
Assert.assertEquals("has power", expected.hasPower(), actual.hasPower());
if (expected.hasPower()) {
Assert.assertEquals("power", expected.getPower(), actual.getPower());
}
Assert.assertEquals("power", expected.power(), actual.power());
Assert.assertEquals("has cadence", expected.hasCadence(), actual.hasCadence());
if (expected.hasCadence()) {
Assert.assertEquals("cadence", expected.getCadence(), actual.getCadence());
}
Assert.assertEquals("cadence", expected.cadence(), actual.cadence());
}
public void assertEquals(List<TrackPoint> expected, List<TrackPoint> actual) {
@@ -10,11 +10,11 @@ public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
@@ -22,11 +22,11 @@ public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
@@ -8,16 +8,16 @@ import org.junit.Test;
import de.dennisguse.opentracks.data.models.HeartRate;
public class BluetoothHandlerManagerHeartRateTest {
public class BluetoothHandlerHeartRateTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
@@ -26,11 +26,11 @@ public class BluetoothHandlerManagerHeartRateTest {
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
@@ -28,7 +28,6 @@ import java.util.List;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class GpsStatusTest {
@@ -82,16 +81,16 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX, GPS_SIGNAL_BAD), statusList);
}
@@ -106,13 +105,13 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now().minusMillis(1000))));
subject.onNewTrackPoint(Position.of(ok, Instant.now().minusMillis(1000)));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
subject.determineGpsStatusByTime(Instant.now());
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_FIX), statusList);
}
@@ -133,13 +132,13 @@ public class GpsStatusTest {
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_BAD), statusList);
Thread.sleep(100);
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
@@ -126,8 +127,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -201,8 +210,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
//when
@@ -216,8 +233,17 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -249,8 +275,16 @@ public class TrackRecordingServiceRecordingTest {
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(starTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), trackPoints);
}
@@ -278,8 +312,17 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -296,7 +339,7 @@ public class TrackRecordingServiceRecordingTest {
mockAltitudeChange(trackPointCreator, 0);
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorHeartRate("", ""));
sensorManager.sensorDataAggregator.add(new AggregatorHeartRate("", ""));
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
@@ -314,12 +357,28 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7))
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse(sensor3)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -369,32 +428,65 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps2),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -441,30 +533,54 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -499,24 +615,47 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -559,8 +698,16 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -603,24 +750,48 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse(gps2),
45.1, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -638,18 +809,18 @@ public class TrackRecordingServiceRecordingTest {
Track.Id trackId = service.startNewTrack();
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorRunning("", ""));
sensorManager.sensorDataSet.barometer = null;
sensorManager.sensorDataAggregator.add(new AggregatorRunning("", ""));
sensorManager.sensorDataAggregator.barometer = null;
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.ZERO))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(1), Distance.ZERO))); //Should be ignored
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(2), Distance.of(2))));
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -658,12 +829,12 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(3), Distance.of(12))));
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(4), Distance.of(14)))); //Should be ignored
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -672,7 +843,7 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(5), Distance.of(16)))); //Should be ignored
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -691,49 +862,107 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor2)) //First moving TrackPoint: store as the time might be interesting.
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint( //First moving TrackPoint: stored as the time might be interesting.
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor2),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(2),
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor3),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(10),
null,
null,
Cadence.of(3),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(4.0)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.of(4.0),
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps4),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.ZERO)) //Sensor data is now outdated, but we do not fall back to GPS.
.setSensorDistance(Distance.ZERO)
Speed.ZERO), //Sensor data is now outdated, but we do not fall back to GPS.
Distance.ZERO,
null,
null,
Cadence.of(0), //TODO This could be null, right?
null,
null
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
trackPointCreator.getSensorManager().sensorDataSet.barometer = new AggregatorBarometer("", "") {
trackPointCreator.getSensorManager().sensorDataAggregator.barometer = new AggregatorBarometer("", "") {
@Override
public boolean hasReceivedData() {
return true;
@@ -45,6 +45,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
@@ -60,7 +61,7 @@ public class TrackRecordingServiceStateMachineTest {
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mRuntimePermissionRule = TestUtil.createGrantPermissionRule();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -15,7 +15,6 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.time.Duration;
import java.time.Instant;
@@ -34,10 +33,11 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater;
//Due to DateTimeFormatter using NBSP, these tests only work in SDK34+
@@ -84,8 +84,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -117,8 +124,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -150,8 +164,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -183,8 +204,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -198,8 +226,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, null).toString();
@@ -231,8 +266,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, null, null).toString();
@@ -246,8 +288,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, false, data.second, null).toString();
@@ -279,8 +328,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -312,8 +368,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -345,8 +408,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_METER, true, null, null).toString();
@@ -378,8 +448,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -393,8 +470,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, true, data.second, null).toString();
@@ -426,8 +510,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
@@ -441,8 +532,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, false, data.second, null).toString();
@@ -461,8 +559,15 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
@@ -487,8 +592,15 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
@@ -60,8 +60,7 @@ public class PreferencesOpenHelperTest {
// when update to version 2
PreferencesOpenHelper.newInstance(2).check();
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";"
+ PreferencesUtils.getLayoutColumnsByDefault() + ";distance,1,1,0;speed,1,1,0;";
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";2;distance,1,1,0;speed,1,1,0;";
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
@@ -110,7 +109,7 @@ public class PreferencesOpenHelperTest {
PreferencesOpenHelper.newInstance(1).check();
// then custom layout should be equals to default layout.
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsvLine(PreferencesUtils.buildDefaultLayout(), resources);
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(1, customRecordingLayout.size());
@@ -210,7 +210,7 @@ public class PreferencesUtilsTest {
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layouts_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;" + PreferencesUtils.getLayoutColumnsByDefault() + ";"
"road cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1,0;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;"
@@ -258,8 +258,8 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
List<RecordingLayout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(runningProfile, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(runningProfile, resources));
PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated);
@@ -303,7 +303,7 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources);
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(recordingLayoutToBeUpdated);
// then only updated profile is modified in the custom layouts
@@ -89,8 +89,7 @@ public class TrackStatisticsUpdaterTest {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -103,15 +102,18 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), Distance.of(5));
TrackPoint tp5 = new TrackPoint(null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.ofEpochMilli(5000)),
Distance.of(10f),
null,
null,
null,
null,
null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -131,13 +133,10 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), Distance.of(5));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
@@ -158,10 +157,8 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
));
assertEquals(Speed.of(2), subject.getTrackStatistics().maxSpeed());
@@ -169,10 +166,8 @@ public class TrackStatisticsUpdaterTest {
// when
List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6), Speed.of(1), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7), Speed.of(1), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
).forEach(subject::addTrackPoint);
@@ -185,16 +180,32 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 1, Altitude.WGS84.of(0), Instant.ofEpochSecond(50)),
createTrackPoint(0, 2, Altitude.WGS84.of(0), Instant.ofEpochSecond(55)),
@@ -210,21 +221,43 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30))
.setSensorDistance(Distance.ofKilometer(1)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), null, Distance.of(10)),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.ofEpochSecond(30)),
Distance.ofKilometer(1),
null,
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55), null, Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60))
));
@@ -240,8 +273,8 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
subject.addTrackPoint(
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10))
.setSensorDistance(Distance.of(10)));
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10), null, Distance.of(10))
);
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
// then
@@ -252,8 +285,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setSensorDistance(Distance.ZERO));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
Distance.ZERO,
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -262,8 +305,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setSensorDistance(Distance.of(1)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
Distance.of(1),
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -272,8 +325,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(50)),
Distance.of(10),
null,
null,
null,
null,
null
)
);
// then
assertFalse(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -294,15 +357,12 @@ public class TrackStatisticsUpdaterTest {
public void copy_constructor() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2,tp3, tp4));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3, tp4));
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(initial, tp5);
@@ -335,7 +395,13 @@ public class TrackStatisticsUpdaterTest {
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
return new TrackPoint(TrackPoint.Type.TRACKPOINT,
return createTrackPoint(latitude, longitude, altitude, time, null, null);
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time, Speed speed, Distance sensorDistance) {
return new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
time,
latitude,
@@ -344,7 +410,14 @@ public class TrackStatisticsUpdaterTest {
altitude,
null,
null,
null
));
speed
),
sensorDistance,
null,
null,
null,
null,
null
);
}
}
@@ -30,6 +30,7 @@ import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -47,7 +48,7 @@ public class EspressoAggregatedFilterTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
@@ -28,7 +28,7 @@ public class RecordingLayoutTest extends TestCase {
String csv = "running;2;" + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1,0;" + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + context.getString(R.string.stats_custom_layout_altitude_key) + ",0,1,0;" + context.getString(R.string.stats_custom_layout_gain_key) + ",0,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -60,13 +60,12 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;speed,1,1;distance,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -75,13 +74,13 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -90,7 +89,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,distance,total time;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -105,7 +104,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,1,0;distance;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -145,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;" + PreferencesUtils.getLayoutColumnsByDefault() + ";key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
}
}
@@ -104,8 +104,7 @@ public class IntervalStatisticsUpdaterTest {
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
trackPoints.add(TestDataUtil.createTrackPoint(i)
.setAltitudeGainLoss(null));
trackPoints.add(TestDataUtil.createTrackPoint(i, TrackPoint.Type.TRACKPOINT, null));
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints);
@@ -44,6 +44,7 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
@@ -278,15 +279,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getPosition().toLocation());
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.position().toLocation());
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_menu_select_layout) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
List<String> layoutNames = PreferencesUtils.getAllCustomLayoutNames();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setDefaultLayout(layoutNames.get(which)));
List<String> layoutNames = PreferencesUtils.getAllCustomLayouts().stream()
.map(RecordingLayout::getName)
.toList();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setSelectedLayoutName(layoutNames.get(which)));
builder.create().show();
return true;
}
@@ -35,11 +35,11 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
/**
* A fragment to display track chart to the user.
@@ -204,7 +204,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
if (isResumed()) {
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.position().speed(), chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -27,8 +27,8 @@ public record ChartPoint(
chartByDistance
? trackStatistics.totalDistance().toKM_Miles(unitSystem)
: trackStatistics.totalDuration().toMillis(),
trackPoint.hasAltitude()
? Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem)
trackPoint.position().hasAltitude()
? Distance.of(trackPoint.position().altitude().toM()).toM_FT(unitSystem)
: null,
smoothedSpeed != null
? smoothedSpeed.to(unitSystem)
@@ -36,14 +36,14 @@ public record ChartPoint(
smoothedSpeed != null
? smoothedSpeed.toPace(unitSystem).toSeconds() / 60d
: null,
trackPoint.hasHeartRate()
? (double) trackPoint.getHeartRate().getBPM()
trackPoint.heartRate() != null
? (double) trackPoint.heartRate().getBPM()
: null,
trackPoint.hasCadence()
? (double) trackPoint.getCadence().getRPM()
trackPoint.cadence() != null
? (double) trackPoint.cadence().getRPM()
: null,
trackPoint.hasPower()
? (double) trackPoint.getPower().getW()
trackPoint.power() != null
? (double) trackPoint.power().getW()
: null
);
}
@@ -49,12 +49,13 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -538,13 +539,19 @@ public class ContentProviderUtils {
int speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
int sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
int temperatureIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_TEMPERATURE);
int sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
int sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
int sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
int altitudeGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_GAIN);
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_LOSS);
TrackPoint trackPoint = new TrackPoint(
AltitudeGainLoss altitudeGainLoss = null;
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
altitudeGainLoss = new AltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return new TrackPoint(
new TrackPoint.Id(cursor.getInt(idIndex)),
TrackPoint.Type.getById(cursor.getInt(typeIndex)),
new Position(
@@ -556,26 +563,14 @@ public class ContentProviderUtils {
!cursor.isNull(accuracyVerticalIndex) ? Distance.of(cursor.getFloat(accuracyVerticalIndex)) : null,
!cursor.isNull(bearingIndex) ? cursor.getFloat(bearingIndex) : null,
!cursor.isNull(speedIndex) ? Speed.of(cursor.getFloat(speedIndex)) : null
));
if (!cursor.isNull(sensorHeartRateIndex)) {
trackPoint.setHeartRate(cursor.getFloat(sensorHeartRateIndex));
}
if (!cursor.isNull(sensorCadenceIndex)) {
trackPoint.setCadence(cursor.getFloat(sensorCadenceIndex));
}
if (!cursor.isNull(sensorDistanceIndex)) {
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(sensorDistanceIndex)));
}
if (!cursor.isNull(sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(sensorPowerIndex));
}
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
trackPoint.setAltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return trackPoint;
),
!cursor.isNull(sensorDistanceIndex) ? Distance.of(cursor.getFloat(sensorDistanceIndex)) : null,
!cursor.isNull(sensorHeartRateIndex) ? HeartRate.of(cursor.getFloat(sensorHeartRateIndex)) : null,
!cursor.isNull(temperatureIndex) ? Temperature.of(cursor.getFloat(temperatureIndex)) : null,
!cursor.isNull(sensorCadenceIndex) ? Cadence.of(cursor.getFloat(sensorCadenceIndex)) : null,
!cursor.isNull(sensorPowerIndex) ? Power.of(cursor.getFloat(sensorPowerIndex)) : null,
altitudeGainLoss
);
}
//TODO Only used for file import; might be better to replace it.
@@ -637,42 +632,45 @@ public class ContentProviderUtils {
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.id());
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
values.put(TrackPointsColumns.TYPE, trackPoint.type().type_db);
if (trackPoint.hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getPosition().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getPosition().longitude() * 1E6));
if (trackPoint.position().hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.position().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.position().longitude() * 1E6));
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
if (trackPoint.position().hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.position().altitude().toM());
}
if (trackPoint.hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.getHorizontalAccuracy().toM());
if (trackPoint.position().hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.position().horizontalAccuracy().toM());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
if (trackPoint.position().hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.position().speed().toMPS());
}
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
if (trackPoint.position().hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.position().bearing());
}
if (trackPoint.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate().getBPM());
if (trackPoint.heartRate() != null) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.heartRate().getBPM());
}
if (trackPoint.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCadence().getRPM());
if (trackPoint.temperature() != null) {
values.put(TrackPointsColumns.SENSOR_TEMPERATURE, trackPoint.temperature().getCelsius());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
if (trackPoint.cadence() != null) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.cadence().getRPM());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower().getW());
if (trackPoint.sensorDistance() != null) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.sensorDistance().toM());
}
if (trackPoint.power() != null) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.power().getW());
}
if (trackPoint.hasAltitudeGainLoss()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeGainLoss().loss_m());
if (trackPoint.altitudeGainLoss() != null) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.altitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.altitudeGainLoss().loss_m());
}
return values;
}
@@ -29,7 +29,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 40;
private static final int DATABASE_VERSION = 41;
private final Context context;
@@ -84,6 +84,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 38 -> upgradeFrom37to38(db);
case 39 -> upgradeFrom38to39(db);
case 40 -> upgradeFrom39to40(db);
case 41 -> upgradeFrom40to41(db);
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
}
}
@@ -111,6 +112,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 37 -> downgradeFrom38to37(db);
case 38 -> downgradeFrom39to38(db);
case 39 -> downgradeFrom40to39(db);
case 40 -> downgradeFrom41to40(db);
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
}
@@ -734,4 +736,32 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom40to41(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_temperature FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, null, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom41to40(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -33,12 +33,12 @@ import java.util.Set;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
/**
* Track data hub.
@@ -288,14 +288,14 @@ public class TrackDataHub {
}
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
TrackPoint.Id trackPointId = trackPoint.id();
// Stop if past the last wanted point
if (maxPointId != null && trackPointId.id() > maxPointId.id()) {
break;
}
egm2008Correction.correctAltitude(context, trackPoint);
trackPoint = egm2008Correction.correctAltitude(context, trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
@@ -319,7 +319,7 @@ public class TrackDataHub {
}
if (trackPoint != null) {
localLastSeenTrackPointIdId = trackPoint.getId();
localLastSeenTrackPointIdId = trackPoint.id();
}
if (updateSamplingState) {
@@ -1,5 +1,7 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.Nullable;
public record HeartRate(float value) {
public static HeartRate INVALID = HeartRate.of(0.0f);
@@ -7,6 +9,12 @@ public record HeartRate(float value) {
return new HeartRate(value);
}
@Nullable
public static HeartRate ofOrNull(Float value) {
if (value == null) return null;
return new HeartRate(value);
}
public float getBPM() {
return value;
}
@@ -51,10 +51,10 @@ public final class MarkerBuilder {
this.typeLocalized = "";
this.photoUrl = null;
if (!trackPoint.hasLocation())
if (!trackPoint.position().hasLocation())
throw new RuntimeException("Marker requires a trackpoint with a location.");
this.position = trackPoint.getPosition();
this.position = trackPoint.position();
}
public MarkerBuilder(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint, String name, String description, String typeLocalized, Uri photoUrl) {
@@ -17,8 +17,9 @@ public record Position(
@Nullable Float bearing,
@Nullable Speed speed
) {
@Deprecated
public static Position empty() {
return of((Instant) null);
return of(null);
}
public static Position of(@NonNull Instant time) {
@@ -34,10 +35,6 @@ public record Position(
);
}
public static Position of(@NonNull Location location) {
return of(location, Instant.ofEpochMilli(location.getTime()));
}
public static Position of(@NonNull Location location, @NonNull Instant time) {
return new Position(
time,
@@ -132,21 +129,12 @@ public record Position(
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
//TODO Use double
public Position withCoordinates(Double latitude, Double longitude) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
public Position withCoordinates(Position position) {
return new Position(time, position.latitude, position.longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
//TODO Use float
public Position withBearing(Float bearing) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
public Position withHorizontalAccuracy(Distance horizontalAccuracy) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
public Position withVerticalAccuracy(Distance verticalAccuracy) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
}
@@ -0,0 +1,18 @@
package de.dennisguse.opentracks.data.models;
public record Temperature(
float value_celsius
) {
public static Temperature of(float value_celsius) {
return new Temperature(value_celsius);
}
public static Temperature ofCentiCelsius(int value_centi_celsius) {
return Temperature.of((float) (value_centi_celsius / 100.0));
}
public float getCelsius() {
return value_celsius;
}
}
@@ -22,23 +22,175 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Instant;
import java.util.Optional;
/**
* Sensor and/or location information for a specific point in time.
* <p>
* Time is created using the {@link de.dennisguse.opentracks.services.handlers.MonotonicClock}, because system time jump backwards.
* GPS time is ignored as for non-GPS events, we could not create GPS-based timestamps.
* GPS time is ignored as for non-GPS events, we cannot create GPS-based timestamps.
*/
//TODO Should be a record (with final properties)
public class TrackPoint {
public record TrackPoint(
@Nullable
private final TrackPoint.Id id;
@Nullable
TrackPoint.Id id,
@NonNull
TrackPoint.Type type,
//Requires: position.time must be non-null
@NonNull
Position position,
Distance sensorDistance,
HeartRate heartRate,
Temperature temperature,
Cadence cadence,
Power power,
AltitudeGainLoss altitudeGainLoss
) {
public TrackPoint(Type type, Position position) {
this(
null,
type,
position,
null,
null,
null,
null,
null,
null
);
}
@Deprecated //TOOD Private?
public TrackPoint(Type type, Instant now) {
this(
null,
type,
Position.of(now),
null,
null,
null,
null,
null,
null
);
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time);
}
public static TrackPoint with(TrackPoint trackPoint, Altitude.EGM2008 correctAltitude) {
return new TrackPoint(
trackPoint.id,
trackPoint.type,
new Position(
trackPoint.position.time(),
trackPoint.position.latitude(),
trackPoint.position.longitude(),
trackPoint.position.horizontalAccuracy(),
correctAltitude,
trackPoint.position.verticalAccuracy(),
trackPoint.position.bearing(),
trackPoint.position.speed()
),
trackPoint.sensorDistance,
trackPoint.heartRate,
null,
trackPoint.cadence,
trackPoint.power,
trackPoint.altitudeGainLoss
);
}
public TrackPoint with(Type newType) {
return new TrackPoint(
id,
newType,
position,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
);
}
public TrackPoint with(Position newPosition) {
return new TrackPoint(
id,
type,
newPosition,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
);
}
//Requires: position.time must be non-null
@NonNull
private Position position;
public Instant getTime() {
return position.time();
}
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
Distance newSensorDistance = sensorDistance;
if (sensorDistance != null && lastTrackPoint.sensorDistance != null) {
newSensorDistance = sensorDistance.minus(lastTrackPoint.sensorDistance);
}
AltitudeGainLoss newAltitudeGainLoss = altitudeGainLoss;
if (altitudeGainLoss != null && lastTrackPoint.altitudeGainLoss() != null) {
newAltitudeGainLoss = new AltitudeGainLoss(
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
}
return new TrackPoint(
id,
type,
position,
newSensorDistance,
heartRate,
null,
cadence,
power,
newAltitudeGainLoss
);
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (sensorDistance() != null) {
return sensorDistance();
}
return distanceToPreviousFromLocation(previous);
}
@NonNull
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
if (!position.hasLocation() || !previous.position().hasLocation()) {
throw new RuntimeException("Cannot compute distance.");
}
return Distance.of(position().toLocation().distanceTo(previous.position().toLocation()));
}
public boolean isIdleTriggered() {
return type == TrackPoint.Type.IDLE;
}
public boolean isSegmentManualStart() {
return type == TrackPoint.Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentManualEnd() {
return type == TrackPoint.Type.SEGMENT_END_MANUAL;
}
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -75,298 +227,6 @@ public class TrackPoint {
}
}
@NonNull
private Type type;
private Distance sensorDistance;
private HeartRate heartRate = null;
private Cadence cadence = null;
private Power power = null;
private AltitudeGainLoss altitudeGainLoss = null;
public TrackPoint(@Nullable TrackPoint.Id id, @NonNull Type type, @NonNull Position position) {
this.id = id;
this.type = type;
this.position = position;
}
public TrackPoint(@NonNull Type type, @NonNull Instant time) {
this(null, type, Position.of(time));
}
public TrackPoint(@NonNull Type type, @NonNull Position position) {
this(null, type, position);
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
}
public static TrackPoint createSegmentEndWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
}
@NonNull
public Type getType() {
return type;
}
@Deprecated //Should not be needed.
public TrackPoint setType(@NonNull Type type) {
this.type = type;
return this;
}
public boolean isIdleTriggered() {
return type == Type.IDLE;
}
public boolean isSegmentManualStart() {
return type == Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentManualEnd() {
return type == Type.SEGMENT_END_MANUAL;
}
//TODO Check if naming is correct.
public boolean wasCreatedManually() {
return hasLocation() || hasSpeed();
}
/**
* May be null if the track was not loaded from the database.
*/
@Nullable
public TrackPoint.Id getId() {
return id;
}
public boolean hasLocation() {
return position.hasLocation();
}
@NonNull
public Position getPosition() {
return position;
}
public TrackPoint setPosition(Position position) {
this.position = position.with(this.position.time());
return this;
}
public boolean hasAltitudeGainLoss() {
return altitudeGainLoss != null;
}
public AltitudeGainLoss getAltitudeGainLoss() {
return altitudeGainLoss;
}
public TrackPoint setAltitudeGainLoss(AltitudeGainLoss altitudeGainLoss) {
this.altitudeGainLoss = altitudeGainLoss;
return this;
}
public TrackPoint setAltitudeGainLoss(float altitudeGain_m, float altitudeLoss_m) {
setAltitudeGainLoss(new AltitudeGainLoss(altitudeGain_m, altitudeLoss_m));
return this;
}
@NonNull
public Instant getTime() {
return position.time();
}
public boolean hasAltitude() {
return position.hasAltitude();
}
public Altitude getAltitude() {
return position.altitude();
}
@Deprecated
public TrackPoint setAltitude(Altitude altitude) {
position = position.with(altitude);
return this;
}
public boolean hasSpeed() {
return position.hasSpeed();
}
public Speed getSpeed() {
return position.speed();
}
@Deprecated
public TrackPoint setSpeed(Speed speed) {
this.position = position.with(speed);
return this;
}
public boolean hasBearing() {
return position.hasBearing();
}
public float getBearing() {
return position.bearing();
}
public TrackPoint setBearing(Float bearing) {
this.position = this.position.withBearing(bearing);
return this;
}
public boolean hasHorizontalAccuracy() {
return position.hasHorizontalAccuracy();
}
public Distance getHorizontalAccuracy() {
return position.horizontalAccuracy();
}
@Deprecated
public TrackPoint setHorizontalAccuracy(Distance horizontalAccuracy) {
this.position = this.position.withHorizontalAccuracy(horizontalAccuracy);
return this;
}
public boolean hasVerticalAccuracy() {
return position.hasVerticalAccuracy();
}
public Distance getVerticalAccuracy() {
return position.verticalAccuracy();
}
public TrackPoint setVerticalAccuracy(Distance verticalAccuracy) {
this.position = this.position.withVerticalAccuracy(verticalAccuracy);
return this;
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return distanceToPreviousFromLocation(previous);
}
@NonNull
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
if (!hasLocation() || hasLocation() != previous.hasLocation()) {
throw new RuntimeException("Cannot compute distance.");
}
return Distance.of(getPosition().toLocation().distanceTo(previous.getPosition().toLocation()));
}
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
return position.fulfillsAccuracy(thresholdHorizontalAccuracy);
}
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
if (!dest.hasLocation() || !hasLocation()) {
return Optional.empty();
}
return Optional.of(getPosition().toLocation().bearingTo(dest.getPosition().toLocation()));
}
// Sensor data
public boolean hasSensorDistance() {
return sensorDistance != null;
}
public Distance getSensorDistance() {
return sensorDistance;
}
public TrackPoint setSensorDistance(Distance distance_m) {
this.sensorDistance = distance_m;
return this;
}
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
if (hasSensorDistance() && lastTrackPoint.hasSensorDistance()) {
sensorDistance = sensorDistance.minus(lastTrackPoint.getSensorDistance());
}
if (hasAltitudeGainLoss() && lastTrackPoint.hasAltitudeGainLoss()) {
altitudeGainLoss = new AltitudeGainLoss(
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
}
return this;
}
public boolean hasHeartRate() {
return heartRate != null;
}
public HeartRate getHeartRate() {
return heartRate;
}
public TrackPoint setHeartRate(HeartRate heartRate) {
this.heartRate = heartRate;
return this;
}
public TrackPoint setHeartRate(float heartRate) {
return setHeartRate(HeartRate.of(heartRate));
}
public boolean hasCadence() {
return cadence != null;
}
public Cadence getCadence() {
return cadence;
}
public TrackPoint setCadence(Cadence cadence) {
this.cadence = cadence;
return this;
}
public TrackPoint setCadence(float cadence) {
return setCadence(Cadence.of(cadence));
}
public boolean hasPower() {
return power != null;
}
public Power getPower() {
return power;
}
public TrackPoint setPower(Power power) {
this.power = power;
return this;
}
public TrackPoint setPower(float power) {
return setPower(Power.of(power));
}
@NonNull
@Override
public String toString() {
return "TrackPoint{" +
"id=" + id +
", type=" + type +
", position=" + position +
", sensorDistance=" + sensorDistance +
", heartRate=" + heartRate +
", cadence=" + cadence +
", power=" + power +
", altitudeGainLoss=" + altitudeGainLoss +
'}';
}
public record Id(long id) implements Parcelable {
@Override
@@ -154,7 +154,7 @@ public class SegmentStatisticUpdater {
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingDuration.isNegative()) {
throw new RuntimeException("Moving time cannot be negative");
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
}
this.movingDuration = this.movingDuration.plus(movingDuration);
}
@@ -111,32 +111,32 @@ public class TrackStatisticsUpdater {
currentSegment.updateTotalTime(trackPoint.getTime());
// Process sensor data: barometer
if (trackPoint.hasAltitudeGainLoss()) {
currentSegment.addTotalAltitudeGainLoss(trackPoint.getAltitudeGainLoss());
if (trackPoint.altitudeGainLoss() != null) {
currentSegment.addTotalAltitudeGainLoss(trackPoint.altitudeGainLoss());
}
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
// Update absolute (GPS-based) altitude
if (trackPoint.position().hasAltitude()) {
currentSegment.updateAltitudeExtremities(trackPoint.position().altitude());
}
// Update heart rate
if (trackPoint.hasHeartRate() && lastTrackPoint != null) {
if (trackPoint.heartRate() != null && lastTrackPoint != null) {
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
Duration newTotalDuration = totalHeartRateDuration.plus(trackPointDuration);
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.getHeartRate().getBPM()) / newTotalDuration.toMillis();
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.heartRate().getBPM()) / newTotalDuration.toMillis();
totalHeartRateDuration = newTotalDuration;
currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM));
}
// Update power
if (trackPoint.hasPower() && lastTrackPoint != null) {
if (trackPoint.power() != null && lastTrackPoint != null) {
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
Duration newTotalDuration = totalPowerDuration.plus(trackPointDuration);
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.getPower().getW()) / newTotalDuration.toMillis();
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.power().getW()) / newTotalDuration.toMillis();
totalPowerDuration = newTotalDuration;
currentSegment.setAveragePower(Power.of(averagePowerW));
@@ -145,11 +145,11 @@ public class TrackStatisticsUpdater {
{
// Update total distance
Distance movingDistance = null;
if (trackPoint.hasSensorDistance()) {
movingDistance = trackPoint.getSensorDistance();
if (trackPoint.sensorDistance() != null) {
movingDistance = trackPoint.sensorDistance();
} else if (lastTrackPoint != null
&& lastTrackPoint.hasLocation()
&& trackPoint.hasLocation()) {
&& lastTrackPoint.position().hasLocation()
&& trackPoint.position().hasLocation()) {
// GPS-based distance/speed
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
}
@@ -173,7 +173,7 @@ public class TrackStatisticsUpdater {
}
}
if (trackPoint.hasSpeed()) {
if (trackPoint.position().hasSpeed()) {
updateSpeed(trackPoint);
}
}
@@ -205,7 +205,7 @@ public class TrackStatisticsUpdater {
* Updates a speed reading while assuming the user is moving.
*/
private void updateSpeed(@NonNull TrackPoint trackPoint) {
Speed currentSpeed = trackPoint.getSpeed();
Speed currentSpeed = trackPoint.position().speed();
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(currentSpeed);
}
@@ -215,8 +215,8 @@ public class TrackStatisticsUpdater {
@Override
public String toString() {
return "TrackStatisticsUpdater{" +
"segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
", currentSegment=" + currentSegment +
"currentSegment=" + currentSegment +
", segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
", lastTrackPoint=" + lastTrackPoint +
'}';
}
@@ -50,6 +50,7 @@ public interface TrackPointsColumns extends BaseColumns {
String SPEED = "speed";
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_TEMPERATURE = "sensor_temperature";
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
@@ -75,6 +76,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SPEED + " FLOAT, "
+ BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_TEMPERATURE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
@@ -90,20 +90,20 @@ public class CSVTrackExporter implements TrackExporter {
public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
List<Column> columns = List.of(
new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().latitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().longitude()) : ""),
new Column("altitude", t -> t.hasAltitude() ? ALTITUDE_FORMAT.format(t.getAltitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
new Column("trackpoint_type", t -> quote(t.type().name())),
new Column("latitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().latitude()) : ""),
new Column("longitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().longitude()) : ""),
new Column("altitude", t -> t.position().hasAltitude() ? ALTITUDE_FORMAT.format(t.position().altitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.position().hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.position().horizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.position().hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.position().verticalAccuracy().toM()) : ""),
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().gain_m()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().loss_m()) : ""),
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
new Column("power", t -> t.hasPower() ? POWER_FORMAT.format(t.getPower().getW()) : ""));
new Column("speed", t -> t.position().hasSpeed() ? SPEED_FORMAT.format(t.position().speed().toKMH()) : ""),
new Column("altitude_gain", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().gain_m()) : ""),
new Column("altitude_loss", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().loss_m()) : ""),
new Column("sensor_distance", t -> t.sensorDistance() != null ? DISTANCE_FORMAT.format(t.sensorDistance().toM()) : ""),
new Column("heartrate", t -> t.heartRate() != null ? HEARTRATE_FORMAT.format(t.heartRate().getBPM()) : ""),
new Column("cadence", t -> t.cadence() != null ? CADENCE_FORMAT.format(t.cadence().getRPM()) : ""),
new Column("power", t -> t.power() != null ? POWER_FORMAT.format(t.power().getW()) : ""));
try {
prepare(outputStream);
@@ -61,6 +61,7 @@ public class GPXTrackExporter implements TrackExporter {
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat TEMPERATURE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -82,6 +83,9 @@ public class GPXTrackExporter implements TrackExporter {
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
TEMPERATURE_FORMAT.setMaximumFractionDigits(2);
TEMPERATURE_FORMAT.setGroupingUsed(false);
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
@@ -140,7 +144,7 @@ public class GPXTrackExporter implements TrackExporter {
wroteTrack = true;
}
switch (trackPoint.getType()) {
switch (trackPoint.type()) {
case SEGMENT_START_MANUAL ->
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
case SEGMENT_END_MANUAL -> {
@@ -152,7 +156,7 @@ public class GPXTrackExporter implements TrackExporter {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
if (trackPoint.hasLocation()) {
if (trackPoint.position().hasLocation()) {
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
} else {
@@ -165,7 +169,7 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment();
wroteSegment = true;
}
if (trackPoint.hasLocation()) {
if (trackPoint.position().hasLocation()) {
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
} else {
@@ -176,7 +180,7 @@ public class GPXTrackExporter implements TrackExporter {
// Not supported as IDLE-TrackPoints have no location.
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
}
}
@@ -306,10 +310,10 @@ public class GPXTrackExporter implements TrackExporter {
private Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
Distance cumulativeDistance;
printWriter.println("<trkpt " + formatLocation(trackPoint.getPosition()) + ">");
printWriter.println("<trkpt " + formatLocation(trackPoint.position()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
if (trackPoint.position().hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.position().altitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
@@ -317,41 +321,45 @@ public class GPXTrackExporter implements TrackExporter {
{
String trackPointExtensionV2Content = "";
if (trackPoint.hasHeartRate()) {
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate().getBPM()) + "</gpxtpx:hr>\n";
if (trackPoint.heartRate() != null) {
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.heartRate().getBPM()) + "</gpxtpx:hr>\n";
}
if (trackPoint.hasCadence()) {
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence().getRPM()) + "</gpxtpx:cad>\n";
if (trackPoint.temperature() != null) {
trackPointExtensionV2Content += "<gpxtpx:atemp>" + TEMPERATURE_FORMAT.format(trackPoint.temperature().getCelsius()) + "</gpxtpx:atemp>\n";
}
if (trackPoint.hasSpeed()) {
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
if (trackPoint.cadence() != null) {
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.cadence().getRPM()) + "</gpxtpx:cad>\n";
}
if (trackPoint.position().hasSpeed()) {
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.position().speed().toMPS()) + "</gpxtpx:speed>\n";
}
String extensionContent = "";
if (trackPoint.hasPower()) {
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
if (trackPoint.power() != null) {
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.power().getW()) + "</pwr:PowerInWatts>\n";
}
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().gain_m() : null);
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().gain_m() : null);
if (cumulativeGain != null) {
extensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().loss_m() : null);
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().loss_m() : null);
if (cumulativeLoss != null) {
extensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
if (trackPoint.hasHorizontalAccuracy()) {
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
if (trackPoint.position().hasHorizontalAccuracy()) {
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.position().horizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
}
if (trackPoint.hasVerticalAccuracy()) {
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
if (trackPoint.position().hasVerticalAccuracy()) {
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.position().verticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
}
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.sensorDistance() != null ? tp.sensorDistance().toM() : null));
if (cumulativeDistance != null) {
extensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
extensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
@@ -67,8 +67,9 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_TRACKPOINT = "trackpoint_type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEARTRATE = "heartrate";
public static final String EXTENDED_DATA_TYPE_TEMPERATURE = "temperature";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_LOSS = "elevation_loss";
@@ -95,7 +96,8 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>();
private final List<Float> temperatureList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>(); //TODO Join with altitudeLossList
private final List<Float> altitudeLossList = new ArrayList<>();
private final List<Float> accuracyHorizontal = new ArrayList<>();
private final List<Float> accuracyVertical = new ArrayList<>();
@@ -167,7 +169,7 @@ public class KMLTrackExporter implements TrackExporter {
wroteTrack = true;
}
switch (trackPoint.getType()) {
switch (trackPoint.type()) {
case SEGMENT_START_MANUAL, SEGMENT_START_AUTOMATIC -> {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
@@ -190,7 +192,7 @@ public class KMLTrackExporter implements TrackExporter {
writeTrackPoint(track.zoneOffset(), trackPoint);
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
}
}
@@ -237,7 +239,7 @@ public class KMLTrackExporter implements TrackExporter {
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd
http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
"""); //TODO ADD xsi:schemaLocation for atom
"""); //NOTE: There is no XSD for Atom
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
@@ -254,6 +256,7 @@ public class KMLTrackExporter implements TrackExporter {
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_HEARTRATE, context.getString(R.string.description_sensor_heart_rate));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_TEMPERATURE, context.getString(R.string.description_sensor_temperature));
printWriter.println("</Schema>");
}
@@ -321,6 +324,7 @@ public class KMLTrackExporter implements TrackExporter {
powerList.clear();
cadenceList.clear();
heartRateList.clear();
temperatureList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
accuracyHorizontal.clear();
@@ -349,6 +353,9 @@ public class KMLTrackExporter implements TrackExporter {
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(heartRateList, EXTENDED_DATA_TYPE_HEARTRATE);
}
if (temperatureList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(temperatureList, EXTENDED_DATA_TYPE_TEMPERATURE);
}
if (altitudeGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
}
@@ -370,28 +377,29 @@ public class KMLTrackExporter implements TrackExporter {
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
trackpointTypeList.add(trackPoint.getType());
trackpointTypeList.add(trackPoint.type());
if (trackPoint.hasLocation()) {
printWriter.println("<coord>" + getCoordinates(trackPoint.getPosition(), " ") + "</coord>");
if (trackPoint.position().hasLocation()) {
printWriter.println("<coord>" + getCoordinates(trackPoint.position(), " ") + "</coord>");
} else {
printWriter.println("<coord/>");
}
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
speedList.add(trackPoint.position().hasSpeed() ? (float) trackPoint.position().speed().toMPS() : null);
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate().getBPM() : null);
cadenceList.add(trackPoint.hasCadence() ? trackPoint.getCadence().getRPM() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower().getW() : null);
if (trackPoint.hasAltitudeGainLoss()) {
altitudeGainList.add(trackPoint.getAltitudeGainLoss().loss_m());
altitudeLossList.add(trackPoint.getAltitudeGainLoss().loss_m());
distanceList.add(trackPoint.sensorDistance() != null ? (float) trackPoint.sensorDistance().toM() : null);
heartRateList.add(trackPoint.heartRate() != null ? trackPoint.heartRate().getBPM() : null);
temperatureList.add(trackPoint.temperature() != null ? trackPoint.temperature().getCelsius() : null);
cadenceList.add(trackPoint.cadence() != null ? trackPoint.cadence().getRPM() : null);
powerList.add(trackPoint.power() != null ? trackPoint.power().getW() : null);
if (trackPoint.altitudeGainLoss() != null) {
altitudeGainList.add(trackPoint.altitudeGainLoss().loss_m());
altitudeLossList.add(trackPoint.altitudeGainLoss().loss_m());
} else {
altitudeGainList.add(null);
altitudeLossList.add(null);
}
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
accuracyHorizontal.add(trackPoint.position().hasHorizontalAccuracy() ? (float) trackPoint.position().horizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.position().hasVerticalAccuracy() ? (float) trackPoint.position().verticalAccuracy().toM() : null);
}
private void writeSimpleArraySensorData(List<Float> list, String name) {
@@ -32,10 +32,15 @@ import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
@@ -79,6 +84,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
@@ -109,6 +115,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String time;
private String speed;
private String heartrate;
private String temperature;
private String cadence;
private String power;
private String markerType;
@@ -214,6 +221,11 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
heartrate = content.trim();
}
}
case TAG_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
}
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
@@ -265,8 +277,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
return;
}
TrackPoint first = currentSegment.getFirst();
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
TrackPoint first = currentSegment.removeFirst();
currentSegment.addFirst(first.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC));
trackImporter.addTrackPoints(currentSegment);
currentSegment.clear();
@@ -332,59 +344,80 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
}
TrackPoint trackPoint = new TrackPoint(
TrackPoint.Type.TRACKPOINT,
new Position(
parsedTime.toInstant(),
latitudeParsed,
longitudeParsed,
accuracyHorizontalParsed,
altitudeParsed,
accuracyVerticalParsed,
null,
speedParsed
));
if (heartrate != null) {
try {
trackPoint.setHeartRate(Float.parseFloat(heartrate));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCadence(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null && loss != null) {
try {
trackPoint.setAltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
}
}
Distance parsedSensorDistance = null;
if (sensorDistance != null) {
try {
trackPoint.setSensorDistance(Distance.of(Float.parseFloat(sensorDistance)));
parsedSensorDistance = Distance.of(Float.parseFloat(sensorDistance));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
}
}
return trackPoint;
HeartRate parsedHeartRate = null;
if (heartrate != null) {
try {
parsedHeartRate = HeartRate.of(Float.parseFloat(heartrate));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
Temperature parsedTemperature = null;
if (temperature != null) {
try {
parsedTemperature = Temperature.of(Float.parseFloat(temperature));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse temperature: %s", temperature)), e);
}
}
Cadence parsedCadence = null;
if (cadence != null) {
try {
parsedCadence = Cadence.of(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
Power parsedPower = null;
if (power != null) {
try {
parsedPower = Power.of(Float.parseFloat(power));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
AltitudeGainLoss altitudeGainLoss = null;
if (gain != null && loss != null) {
try {
altitudeGainLoss = new AltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
}
}
return new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
parsedTime.toInstant(),
latitudeParsed,
longitudeParsed,
accuracyHorizontalParsed,
altitudeParsed,
accuracyVerticalParsed,
null,
speedParsed
),
parsedSensorDistance,
parsedHeartRate,
parsedTemperature,
parsedCadence,
parsedPower,
altitudeGainLoss
);
}
private void onTrackPointStart(Attributes attributes) {
@@ -400,9 +433,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorDistance = null;
accuracyHorizontal = null;
accuracyVertical = null;
power = null;
heartrate = null;
temperature = null;
cadence = null;
power = null;
}
private void onMarkerStart(Attributes attributes) {
@@ -425,8 +459,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getPosition());
if (!trackPoint.position().hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position());
return;
}
markers.add(new Marker(
@@ -435,7 +469,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
name != null ? name : "",
description != null ? description : "",
markerType != null ? markerType : "",
trackPoint.getPosition(),
trackPoint.position(),
photoUrl
));
}
@@ -25,6 +25,7 @@ public class ImportWorker extends Worker {
private static final String TAG = ImportWorker.class.getSimpleName();
static final String URI_KEY = "DIRECTORY_URI_KEY";
static final String FILENAME_KEY = "FILENAME_KEY";
static final String RESULT_SUCCESS_LIST_TRACKIDS_KEY = "RESULT_TRACK_IDS";
static final String RESULT_URI_KEY = "RESULT_URI";
@@ -32,11 +33,13 @@ public class ImportWorker extends Worker {
static final String RESULT_FAILURE_IS_DUPLICATE = "RESULT_FAILURE_IS_DUPLICATE";
private final Uri uri;
private final String filename;
public ImportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
uri = Uri.parse(getInputData().getString(URI_KEY));
filename = getInputData().getString(FILENAME_KEY);
}
@NonNull
@@ -53,7 +56,7 @@ public class ImportWorker extends Worker {
.putString(RESULT_URI_KEY, uri.toString());
String fileExtension = FileUtils.getExtension(uri);
String fileExtension = FileUtils.getExtension(filename);
try {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GPXTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
@@ -69,6 +69,7 @@ public class Importer {
WorkRequest importRequest = new OneTimeWorkRequest.Builder(ImportWorker.class)
.setInputData(new Data.Builder()
.putString(ImportWorker.URI_KEY, documentFile.getUri().toString())
.putString(ImportWorker.FILENAME_KEY, documentFile.getName())
.build())
.build();
@@ -33,10 +33,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -79,7 +85,6 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl";
// private static final String TAG_VALUE = "value"; TODO
private static final String TAG_WHEN = "when";
private static final String TAG_UUID = "opentracks:trackid";
@@ -95,15 +100,17 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
private ZoneOffset zoneOffset;
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Position> positionList = new ArrayList<>();
private final ArrayList<LatLngAltitude> positionList = new ArrayList<>();
private String dataType;
//TODO For ArrayList use specific data models; and why Float? Double would be easier.
private final ArrayList<String> trackpointTypeList = new ArrayList<>();
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
private final ArrayList<Float> sensorTemperatureList = new ArrayList<>();
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
@@ -151,8 +158,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
onTrackSegmentStart();
}
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA ->
dataType = attributes.getValue(ATTRIBUTE_NAME);
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA -> dataType = attributes.getValue(ATTRIBUTE_NAME);
}
}
@@ -256,13 +262,21 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
return;
}
Position position = createPosition(latitude, longitude, altitude);
if (position == null) {
LatLngAltitude latLngAltitude = createPosition(latitude, longitude, altitude);
if (latLngAltitude == null) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + latitude + " " + longitude);
return;
}
position = position.with(whenList.get(0));
Position position = new Position(
whenList.get(0),
latLngAltitude.latitude,
latLngAltitude.longitude,
null,
latLngAltitude.altitude,
null,
null,
null
);
markers.add(new Marker(
null,
null,
@@ -300,6 +314,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorSpeedList.clear();
sensorDistanceList.clear();
sensorHeartRateList.clear();
sensorTemperatureList.clear();
sensorCadenceList.clear();
sensorPowerList.clear();
altitudeGainList.clear();
@@ -316,63 +331,86 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
// Close a track segment by inserting the segment locations
for (int i = 0; i < positionList.size(); i++) {
Instant time = whenList.get(i);
Position position = positionList.get(i);
LatLngAltitude latLngAltitude = positionList.get(i);
TrackPoint trackPoint;
if (position == null) {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(time));
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, position.with(time));
}
TrackPoint.Type type = null;
Speed speed = null;
Distance sensorDistance = null;
HeartRate heartRate = null;
Temperature temperature = null;
Cadence cadence = null;
Power power = null;
AltitudeGainLoss altitudeGainLoss = null;
Distance horizontalAccuracy = null;
Distance verticalAccuracy = null;
if (i < trackpointTypeList.size() && trackpointTypeList.get(i) != null) {
TrackPoint.Type type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
trackPoint.setType(type);
type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
}
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
speed = Speed.of(sensorSpeedList.get(i));
}
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
sensorDistance = Distance.of(sensorDistanceList.get(i));
}
if (i < sensorHeartRateList.size() && sensorHeartRateList.get(i) != null) {
trackPoint.setHeartRate(sensorHeartRateList.get(i));
if (i < sensorHeartRateList.size()) {
heartRate = HeartRate.ofOrNull(sensorHeartRateList.get(i));
}
if (i < sensorTemperatureList.size() && sensorTemperatureList.get(i) != null) {
temperature = Temperature.of(sensorTemperatureList.get(i));
}
if (i < sensorCadenceList.size() && sensorCadenceList.get(i) != null) {
trackPoint.setCadence(sensorCadenceList.get(i));
cadence = Cadence.of(sensorCadenceList.get(i));
}
if (i < sensorPowerList.size() && sensorPowerList.get(i) != null) {
trackPoint.setPower(sensorPowerList.get(i));
power = Power.of(sensorPowerList.get(i));
}
if (i < altitudeGainList.size() && i < altitudeLossList.size()) {
if (altitudeGainList.get(i) != null && altitudeLossList.get(i) != null) {
trackPoint.setAltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
}
if ((i < altitudeGainList.size() && altitudeGainList.get(i) != null)
&& (i < altitudeLossList.size() && altitudeLossList.get(i) != null)) {
altitudeGainLoss = new AltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
}
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
horizontalAccuracy = Distance.of(accuracyHorizontal.get(i));
}
if (i < accuracyVertical.size() && accuracyVertical.get(i) != null) {
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical.get(i)));
verticalAccuracy = Distance.of(accuracyVertical.get(i));
}
Position position = new Position(
time,
latLngAltitude != null ? latLngAltitude.latitude : null,
latLngAltitude != null ? latLngAltitude.longitude : null,
horizontalAccuracy,
latLngAltitude != null ? latLngAltitude.altitude : null,
verticalAccuracy,
null,
speed
);
// Update TrackPoint type for START / STOP.
TrackPoint.Type type = trackPoint.getType();
if (i == 0) {
//first
if (!trackPoint.wasCreatedManually()) {
type = TrackPoint.Type.SEGMENT_START_MANUAL;
} else {
if (wasCreatedAutomatically(position)) {
type = TrackPoint.Type.SEGMENT_START_AUTOMATIC;
} else {
type = TrackPoint.Type.SEGMENT_START_MANUAL;
}
} else if (i == positionList.size() - 1 && !trackPoint.wasCreatedManually()) {
} else if (i == positionList.size() - 1 && !wasCreatedAutomatically(position)) {
//last
type = TrackPoint.Type.SEGMENT_END_MANUAL;
}
trackPoint.setType(type);
trackImporter.addTrackPoint(trackPoint);
trackImporter.addTrackPoint(new TrackPoint(
null,
type != null ? type : TrackPoint.Type.TRACKPOINT,
position,
sensorDistance,
heartRate,
temperature,
cadence,
power,
altitudeGainLoss
));
}
}
@@ -391,7 +429,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
altitude = null;
}
private Position createPosition(String latitude, String longitude, String altitude) {
private LatLngAltitude createPosition(String latitude, String longitude, String altitude) {
//TODO Do not use Location.
if (longitude != null && latitude != null) {
Location location = new Location("import");
try {
@@ -408,7 +447,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
return Position.of(location, null);
return new LatLngAltitude(location.getLatitude(), location.getLongitude(), Altitude.WGS84.of(location.getAltitude()));
}
return null;
@@ -436,15 +475,13 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER -> sensorPowerList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEARTRATE,
EXTENDED_DATA_TYPE_HEART_RATE_LEGACY -> sensorHeartRateList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_TEMPERATURE -> sensorTemperatureList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE -> sensorCadenceList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN -> altitudeGainList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS -> altitudeLossList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL ->
accuracyHorizontal.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL ->
accuracyVertical.add(value);
default ->
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL -> accuracyHorizontal.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL -> accuracyVertical.add(value);
default -> Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
}
}
@@ -457,6 +494,10 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
trackImporter.finish();
}
public boolean wasCreatedAutomatically(Position position) {
return position.hasLocation() || position.hasSpeed();
}
@Override
public DefaultHandler getHandler() {
return this;
@@ -471,4 +512,11 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
public void cleanImport() {
trackImporter.cleanImport();
}
private record LatLngAltitude(
double latitude,
double longitude,
Altitude altitude
) {
}
}
@@ -176,21 +176,19 @@ public class TrackImporter {
TrackPoint previous = trackPoints.get(i - 1);
TrackPoint current = trackPoints.get(i);
if (current.hasSensorDistance() || (previous.hasLocation() && current.hasLocation())) {
if (current.sensorDistance() != null || (previous.position().hasLocation() && current.position().hasLocation())) {
TrackPoint newCurrent = current;
Distance distanceToPrevious = current.distanceToPrevious(previous);
if (!current.hasSpeed()) {
if (!current.position().hasSpeed()) {
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
newCurrent = newCurrent.with(newCurrent.position().with(Speed.of(distanceToPrevious, timeDifference)));
}
if (!current.hasBearing()) {
previous.bearingTo(current)
.ifPresent(current::setBearing);
}
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
if (current.type().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
newCurrent = newCurrent.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
trackPoints.set(i, newCurrent);
}
}
}
@@ -12,10 +12,10 @@ import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL) //TODO extract leastSigBits
);
@Override
@@ -37,9 +37,7 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
}
/**
* Decoding:
* org.bluetooth.service.environmental_sensing.xml
* org.bluetooth.characteristic.pressure.xml
* Decoding: org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
@@ -14,7 +14,7 @@ public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
BluetoothHandlerManagerCyclingPower.CYCLING_POWER,
BluetoothHandlerCyclingPower.CYCLING_POWER,
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
);
@@ -30,8 +30,8 @@ public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerCyclingPower.Data data = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
observer.onChange(new Raw<>(observer.getNow(), data.crank()));
}
@@ -13,7 +13,7 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterface {
public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL),
@@ -13,7 +13,7 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface {
public class BluetoothHandlerHeartRate implements SensorHandlerInterface {
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
@@ -0,0 +1,53 @@
package de.dennisguse.opentracks.sensors;
import static de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure.ENVIRONMENTAL_SENSING_SERVICE;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerTemperature implements SensorHandlerInterface {
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(TEMPERATURE);
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorTemperature(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Temperature value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(observer.getNow(), value));
}
/**
* Decoding: org.bluetooth.characteristic.temperature.xml
*/
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 2) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
return Temperature.ofCentiCelsius(pressure);
}
}
@@ -24,8 +24,6 @@ import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -40,13 +38,12 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final Context context;
private final Handler handler;
private boolean started = false;
private final BluetoothConnectionManager heartRate;
private final BluetoothConnectionManager temperature;
private final BluetoothConnectionManager cyclingCadence;
private final BluetoothConnectionManager cyclingSpeed;
private final BluetoothConnectionManager cyclingPower;
@@ -57,10 +54,11 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
this.handler = handler;
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerHeartRate());
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerHeartRate());
this.temperature = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerTemperature());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerCyclingPower());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@@ -75,6 +73,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
@Override
public synchronized void stop(Context context) {
heartRate.disconnect();
temperature.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
@@ -96,6 +95,11 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
connect(heartRate, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_temperature_key, key)) {
String address = PreferencesUtils.getBluetoothTemperatureSensorAddress();
connect(temperature, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress();
connect(cyclingCadence, address);
@@ -19,7 +19,6 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@@ -124,20 +123,20 @@ public class GpsManager implements SensorConnector, LocationListenerCompat, GpsS
@Override
public void onLocationChanged(@NonNull Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(location, trackPointCreator.createNow()));
gpsStatusManager.onNewTrackPoint(trackPoint);
Position position = Position.of(location, trackPointCreator.createNow());
gpsStatusManager.onNewTrackPoint(position);
if (!trackPoint.getPosition().hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
if (!position.hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
return;
}
if (!trackPoint.getPosition().fulfillsAccuracy(thresholdHorizontalAccuracy)) {
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
observer.onChange(new Raw<>(observer.getNow(), Position.of(location)));
observer.onChange(new Raw<>(observer.getNow(), position));
}
@Override
@@ -12,7 +12,7 @@ import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -35,7 +35,7 @@ class GpsStatusManager {
private final Context context;
@Nullable
private TrackPoint lastTrackPoint = null;
private Position lastPosition = null;
private Handler handler;
@@ -81,26 +81,24 @@ class GpsStatusManager {
}
/**
* This method must be called from the client every time a new trackPoint is received.
* Receive new trackPoint and calculate the new status if needed.
* This method must be called every time a new {@link Position} is received.
* Receive new position and calculate the new status if needed.
* It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any.
*/
public void onNewTrackPoint(@NonNull final TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onNewTrackPoint(@NonNull Position position) {
lastPosition = position;
determineGpsStatusOnTrackpoint(trackPoint);
determineGpsStatusOnTrackpoint(position);
}
/**
* Checks if lastLocation has new GPS status looking up time and accuracy.
* Checks if {@link Position} has new GPS status looking up time and accuracy.
* It depends of signalLostThreshold and signalBadThreshold.
* If there is any change then it does the change.
* Also, it'll run the runnable if signal is bad or stop it if the signal is lost.
*/
//TODO use MonotonicClock instead of Instant.now()
@VisibleForTesting
void determineGpsStatusOnTrackpoint(@NonNull TrackPoint lastTrackPoint) {
if (lastTrackPoint.fulfillsAccuracy(horizontalAccuracyThreshold)) {
private void determineGpsStatusOnTrackpoint(@NonNull Position position) {
if (position.fulfillsAccuracy(horizontalAccuracyThreshold)) {
if (gpsStatus != GpsStatusValue.GPS_SIGNAL_FIX) {
setGpsStatus(GpsStatusValue.GPS_SIGNAL_FIX);
scheduleTimer(); //TODO
@@ -115,10 +113,10 @@ class GpsStatusManager {
}
void determineGpsStatusByTime(Instant now) {
if (lastTrackPoint == null) {
if (lastPosition == null) {
return;
}
if (signalLostThreshold.minus(Duration.between(lastTrackPoint.getTime(), now)).isNegative()) {
if (signalLostThreshold.minus(Duration.between(lastPosition.time(), now)).isNegative()) {
// Too much time without receiving signal -> signal lost.
if (gpsStatus != GpsStatusValue.GPS_SIGNAL_LOST) {
setGpsStatus(GpsStatusValue.GPS_SIGNAL_LOST);
@@ -155,7 +153,7 @@ class GpsStatusManager {
}
setGpsStatus(GpsStatusValue.GPS_DISABLED);
lastTrackPoint = null;
lastPosition = null;
stopTimer();
}
@@ -11,9 +11,9 @@ import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.util.SystemUtils;
@@ -24,7 +24,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
//TODO Should be final and not be visible for testing
@VisibleForTesting
public SensorDataSet sensorDataSet;
public SensorDataAggregator sensorDataAggregator;
private PowerManager.WakeLock wakeLock;
@@ -34,23 +34,23 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
@Override
public void onConnect(Aggregator<?, ?> aggregator) {
sensorDataSet.add(aggregator);
sensorDataAggregator.add(aggregator);
}
@Override
public void onChange(Raw<?> data) {
sensorDataSet.update(data);
observer.onChange(new SensorDataSet(sensorDataSet));
sensorDataAggregator.update(data);
observer.onChange();
}
@Override
public void onDisconnect(Aggregator<?, ?> aggregator) {
sensorDataSet.add(aggregator);
sensorDataAggregator.add(aggregator);
}
@Override
public void onRemove(Aggregator<?, ?> aggregator) {
sensorDataSet.remove(aggregator);
sensorDataAggregator.remove(aggregator);
}
@Override
@@ -67,7 +67,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
public SensorManager(TrackPointCreator observer) {
this.observer = observer;
this.sensorDataSet = new SensorDataSet(observer);
this.sensorDataAggregator = new SensorDataAggregator();
}
public void start(Context context, Handler handler) {
@@ -100,12 +100,11 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
gpsManager.stop(context);
gpsManager = null;
sensorDataSet.clear();
sensorDataAggregator.clear();
}
public SensorDataSet fill(TrackPoint trackPoint) {
sensorDataSet.fillTrackPoint(trackPoint);
return new SensorDataSet(sensorDataSet);
public SensorDataSet getSensorDataSet(Instant now) {
return sensorDataAggregator.getSensorData(now);
}
public void reset() {
@@ -113,7 +112,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
sensorDataSet.reset();
sensorDataAggregator.reset();
}
@VisibleForTesting
@@ -1,15 +1,14 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Pair;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
public abstract class Aggregator<Input, Output> {
private static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
protected Raw<Input> previous;
protected Output aggregatedValue;
@@ -45,6 +44,7 @@ public abstract class Aggregator<Input, Output> {
return aggregatedValue != null;
}
@Deprecated //TODO Remove: for sensor there is no default value. Make getAggregatedValue() @Nullable
@NonNull
protected abstract Output getNoneValue();
@@ -61,8 +61,8 @@ public abstract class Aggregator<Input, Output> {
}
@NonNull
public Pair<Output, String> getAggregatedValueWithSensorName(Instant now) {
return new Pair<>(getAggregatedValue(now), getSensorNameOrAddress());
public SensorData<Output> getAggregatedValueWithSensorName(Instant now) {
return new SensorData<>(getAggregatedValue(now), getSensorNameOrAddress());
}
/**
@@ -86,10 +86,14 @@ public abstract class Aggregator<Input, Output> {
return now
.isAfter(
previous.time()
.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)
.plus(getMaxSensorAge())
);
}
protected Duration getMaxSensorAge() {
return MAX_SENSOR_DATE_SET_AGE;
}
@NonNull
@Override
public String toString() {
@@ -3,16 +3,16 @@ package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
public class AggregatorCyclingPower extends Aggregator<BluetoothHandlerManagerCyclingPower.Data, Power> {
public class AggregatorCyclingPower extends Aggregator<BluetoothHandlerCyclingPower.Data, Power> {
public AggregatorCyclingPower(String name, String address) {
super(name, address);
}
@Override
public void computeValue(Raw<BluetoothHandlerManagerCyclingPower.Data> current) {
public void computeValue(Raw<BluetoothHandlerCyclingPower.Data> current) {
this.aggregatedValue = current.value().power();
}
@@ -2,6 +2,8 @@ package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Position;
public class AggregatorGPS extends Aggregator<Position, Position> {
@@ -36,4 +38,11 @@ public class AggregatorGPS extends Aggregator<Position, Position> {
protected Position getNoneValue() {
return Position.empty();
}
@NonNull
@Override
public Position getAggregatedValue(Instant now) {
return super.getAggregatedValue(now)
.with(now);
}
}
@@ -0,0 +1,34 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Temperature;
public class AggregatorTemperature extends Aggregator<Temperature, Temperature> {
public AggregatorTemperature(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
protected void computeValue(Raw<Temperature> current) {
this.aggregatedValue = current.value();
}
@NonNull
protected Temperature getNoneValue() {
return Temperature.ofCentiCelsius(0);
}
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
public void resetAggregated() {};
@Override
protected Duration getMaxSensorAge() {
return Duration.ofMinutes(1);
}
}
@@ -0,0 +1,9 @@
package de.dennisguse.opentracks.sensors.sensorData;
public record SensorData<T>(
T data,
String sensorNameOrAddress
) {
}
@@ -0,0 +1,229 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class SensorDataAggregator {
private static final String TAG = SensorDataAggregator.class.getSimpleName();
@VisibleForTesting
public AggregatorHeartRate heartRate;
@VisibleForTesting
public AggregatorTemperature temperature;
@VisibleForTesting
public AggregatorCyclingCadence cyclingCadence;
@VisibleForTesting
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
@VisibleForTesting
public AggregatorCyclingPower cyclingPower;
@VisibleForTesting
public AggregatorRunning runningDistanceSpeedCadence;
@VisibleForTesting
public AggregatorBarometer barometer;
public AggregatorGPS gps;
@VisibleForTesting
public SensorData<HeartRate> getHeartRate(Instant now) {
if (heartRate != null) {
SensorData<HeartRate> value = heartRate.getAggregatedValueWithSensorName(now);
if (value.data().isValid()) return value;
}
return null;
}
private SensorData<Cadence> getCadence(Instant now) {
if (cyclingCadence != null) {
return cyclingCadence.getAggregatedValueWithSensorName(now);
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).cadence() != null) {
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
private SensorData<Distance> getDistance(Instant now) {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData()) {
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).distanceOverall(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData()) {
return new SensorData<>(runningDistanceSpeedCadence.getAggregatedValue(now).distance(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
//TOOD simplify?
private SensorData<Speed> getSpeed(Instant now) {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData() && cyclingDistanceSpeed.getAggregatedValue(now).speed() != null) {
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).speed() != null) {
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public void add(@NonNull Aggregator<?, ?> data) {
set(data, data);
}
//TODO Refactor generic "fun" here: go explicit.
public void update(@NonNull Raw<?> data) {
Object value = data.value();
if (value instanceof HeartRate) {
this.heartRate.add((Raw<HeartRate>) data);
return;
}
if (value instanceof Temperature) {
this.temperature.add((Raw<Temperature>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData) {
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
return;
}
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingPower.Data) {
this.cyclingPower.add((Raw<BluetoothHandlerCyclingPower.Data>) data);
return;
}
if (value instanceof AtmosphericPressure) {
this.barometer.add((Raw<AtmosphericPressure>) data);
return;
}
if (value instanceof Position) {
this.gps.add((Raw<Position>) data);
return;
}
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
}
public void remove(@NonNull Aggregator<?, ?> type) {
set(type, null);
}
public void clear() {
Log.i(TAG, "Removing all aggregators");
this.heartRate = null;
this.temperature = null;
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
this.barometer = null;
this.gps = null;
}
public SensorDataSet getSensorData(Instant now) {
return new SensorDataSet(
gps != null && gps.hasReceivedData() ? gps.getAggregatedValueWithSensorName(now) : new SensorData<>(Position.of(now), ""),
getSpeed(now),
getDistance(now),
getHeartRate(now),
temperature != null && temperature.hasReceivedData() ? temperature.getAggregatedValueWithSensorName(now) : null,
getCadence(now),
cyclingPower != null && cyclingPower.hasReceivedData() ? cyclingPower.getAggregatedValueWithSensorName(now) : null,
barometer != null && barometer.hasReceivedData() ? barometer.getAggregatedValueWithSensorName(now) : null
);
}
public void reset() {
Log.i(TAG, "Resetting data");
if (heartRate != null) heartRate.resetAggregated();
if (temperature != null) temperature.resetAggregated();
if (cyclingCadence != null) cyclingCadence.resetAggregated();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
if (cyclingPower != null) cyclingPower.resetAggregated();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.resetAggregated();
if (barometer != null) barometer.resetAggregated();
if (gps != null) gps.resetAggregated();
}
//TODO Refactor generic "fun" here: go explicit.
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
if (type instanceof AggregatorHeartRate) {
heartRate = (AggregatorHeartRate) sensorData;
return;
}
if (type instanceof AggregatorTemperature) {
temperature = (AggregatorTemperature) sensorData;
return;
}
if (type instanceof AggregatorCyclingCadence) {
cyclingCadence = (AggregatorCyclingCadence) sensorData;
return;
}
if (type instanceof AggregatorCyclingDistanceSpeed) {
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
return;
}
if (type instanceof AggregatorCyclingPower) {
cyclingPower = (AggregatorCyclingPower) sensorData;
return;
}
if (type instanceof AggregatorRunning) {
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
return;
}
if (type instanceof AggregatorBarometer) {
barometer = (AggregatorBarometer) sensorData;
return;
}
if (type instanceof AggregatorGPS) {
gps = (AggregatorGPS) sensorData;
return;
}
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
}
@@ -1,240 +1,32 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.data.models.Temperature;
public class SensorDataSet {
private static final String TAG = SensorDataSet.class.getSimpleName();
@VisibleForTesting
public AggregatorHeartRate heartRate;
@VisibleForTesting
public AggregatorCyclingCadence cyclingCadence;
@VisibleForTesting
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
@VisibleForTesting
public AggregatorCyclingPower cyclingPower;
@VisibleForTesting
public AggregatorRunning runningDistanceSpeedCadence;
@VisibleForTesting
public AggregatorBarometer barometer;
public AggregatorGPS gps;
private final TrackPointCreator trackPointCreator;
public SensorDataSet(TrackPointCreator trackPointCreator) {
this.trackPointCreator = trackPointCreator;
}
@Deprecated //TODO This is not a copy constructor anymore, but it should be - aggregators are no value objects; best guess: can be removed
public SensorDataSet(SensorDataSet toCopy) {
this.trackPointCreator = toCopy.trackPointCreator;
this.heartRate = toCopy.heartRate;
this.cyclingCadence = toCopy.cyclingCadence;
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
this.cyclingPower = toCopy.cyclingPower;
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
this.barometer = toCopy.barometer;
this.gps = toCopy.gps;
}
public Pair<HeartRate, String> getHeartRate() {
if (heartRate != null) {
Pair<HeartRate, String> value = heartRate.getAggregatedValueWithSensorName(trackPointCreator.createNow());
if (value.first.isValid()) return value;
}
return null;
}
public Pair<Cadence, String> getCadence() {
if (cyclingCadence != null) {
return cyclingCadence.getAggregatedValueWithSensorName(trackPointCreator.createNow());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.aggregatedValue.cadence() != null) {
return new Pair<>(runningDistanceSpeedCadence.aggregatedValue.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public Pair<Speed, String> getSpeed() {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData() && cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).speed() != null) {
return new Pair<>(cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(trackPointCreator.createNow()).speed() != null) {
return new Pair<>(runningDistanceSpeedCadence.aggregatedValue.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public AggregatorCyclingPower getCyclingPower() {
return cyclingPower;
}
public void add(@NonNull Aggregator<?, ?> data) {
set(data, data);
}
public void update(@NonNull Raw<?> data) {
Object value = data.value();
if (value instanceof HeartRate) {
this.heartRate.add((Raw<HeartRate>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData ) {
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
return;
}
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
return;
}
if (value instanceof BluetoothHandlerManagerCyclingPower.Data) {
this.cyclingPower.add((Raw<BluetoothHandlerManagerCyclingPower.Data>) data);
return;
}
if (value instanceof AtmosphericPressure) {
this.barometer.add((Raw<AtmosphericPressure>) data);
return;
}
if (value instanceof Position) {
this.gps.add((Raw<Position>) data);
return;
}
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
}
public void remove(@NonNull Aggregator<?, ?> type) {
set(type, null);
}
public void clear() {
Log.i(TAG, "Removing all aggregators");
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
this.barometer = null;
this.gps = null;
}
public void fillTrackPoint(TrackPoint trackPoint) {
if (gps != null && gps.hasReceivedData()) {
trackPoint.setPosition(gps.getAggregatedValue(trackPointCreator.createNow()));
}
Pair<HeartRate, String> heartRate = getHeartRate();
if (heartRate != null) {
trackPoint.setHeartRate(heartRate.first);
}
if (getCadence() != null) {
trackPoint.setCadence(getCadence().first);
}
if (getSpeed() != null) {
trackPoint.setSpeed(getSpeed().first);
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).distanceOverall());
}
if (cyclingPower != null && cyclingPower.hasReceivedData()) {
trackPoint.setPower(cyclingPower.getAggregatedValue(trackPointCreator.createNow()));
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getAggregatedValue(trackPointCreator.createNow()).distance());
}
if (barometer != null && barometer.hasReceivedData()) {
trackPoint.setAltitudeGainLoss(barometer.getAggregatedValue(trackPointCreator.createNow()).gain_m(), barometer.getAggregatedValue(trackPointCreator.createNow()).loss_m());
}
}
public void reset() {
Log.i(TAG, "Resetting data");
if (heartRate != null) heartRate.resetAggregated();
if (cyclingCadence != null) cyclingCadence.resetAggregated();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
if (cyclingPower != null) cyclingPower.resetAggregated();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.resetAggregated();
if (barometer != null) barometer.resetAggregated();
if (gps != null) gps.resetAggregated();
}
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
if (type instanceof AggregatorHeartRate) {
heartRate = (AggregatorHeartRate) sensorData;
return;
}
if (type instanceof AggregatorCyclingCadence) {
cyclingCadence = (AggregatorCyclingCadence) sensorData;
return;
}
if (type instanceof AggregatorCyclingDistanceSpeed) {
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
return;
}
if (type instanceof AggregatorCyclingPower) {
cyclingPower = (AggregatorCyclingPower) sensorData;
return;
}
if (type instanceof AggregatorRunning) {
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
return;
}
if (type instanceof AggregatorBarometer) {
barometer = (AggregatorBarometer) sensorData;
return;
}
if (type instanceof AggregatorGPS) {
gps = (AggregatorGPS) sensorData;
return;
}
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
public record SensorDataSet(
@NonNull
SensorData<Position> position,
@Nullable
SensorData<Speed> speed,
@Nullable
SensorData<Distance> distance,
@Nullable
SensorData<HeartRate> heartRate,
@Nullable
SensorData<Temperature> temperature,
SensorData<Cadence> cadence,
@Nullable
SensorData<Power> power,
@Nullable
SensorData<AltitudeGainLoss> altitudeGainLoss
) {
}
@@ -22,11 +22,11 @@ import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.TrackNameUtils;
public class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChangeListener {
@@ -160,14 +160,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
* @return TrackPoint was stored?
*/
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
if (trackPoint.hasSpeed()) {
if (trackPoint.position().hasSpeed()) {
lastTrackPointUIWithSpeed = trackPoint;
}
if (trackPoint.hasAltitude()) {
if (trackPoint.position().hasAltitude()) {
lastTrackPointUIWithAltitude = trackPoint;
}
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
if (trackPoint.type() == TrackPoint.Type.IDLE) {
insertTrackPoint(trackPoint, true);
handler.removeCallbacks(ON_IDLE);
return true;
@@ -180,14 +180,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
return true;
}
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
if (trackPoint.position().hasLocation() && lastStoredTrackPointWithLocation == null) {
insertTrackPoint(trackPoint, true);
scheduleNewIdleTimeout();
return true;
}
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
if (!trackPoint.position().hasLocation() && trackPoint.sensorDistance() == null) {
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
.isBefore(trackPoint.getTime());
@@ -201,14 +201,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
}
Distance distanceToLastStoredTrackPoint;
if (trackPoint.hasLocation() && !lastStoredTrackPoint.hasLocation()) {
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
} else {
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
}
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPoint = trackPoint.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackPoint, true);
scheduleNewIdleTimeout();
@@ -248,8 +248,8 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time.");
} else {
insertTrackPointHelper(lastTrackPoint);
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
trackPoint.minusCumulativeSensorData(lastTrackPoint);
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
trackPoint = trackPoint.minusCumulativeSensorData(lastTrackPoint);
}
}
lastTrackPoint = null;
@@ -264,7 +264,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
contentProviderUtils.updateTrackStatistics(trackId, trackStatisticsUpdater.getTrackStatistics());
lastStoredTrackPoint = trackPoint;
if (trackPoint.hasLocation()) {
if (trackPoint.position().hasLocation()) {
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
}
} catch (SQLiteException e) {
@@ -255,7 +255,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
"",
"",
"",
trackPoint.getPosition(),
trackPoint.position(),
null
);
return new ContentProviderUtils(this).insertMarker(marker);
@@ -109,17 +109,17 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
.build(context);
//TODO The decision if a trackpoint is not accurate enough should happen in the TrackRecordingManager (via RecordingData).
if (recordingData.latestTrackPoint().hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().getHorizontalAccuracy());
if (recordingData.latestTrackPoint().position().hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().position().horizontalAccuracy());
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().getHorizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().position().horizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(recordingData.trackStatistics().totalDistance())));
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().getSpeed());
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().position().speed());
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
@@ -18,7 +18,6 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.TtsSpan;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,9 +34,10 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.util.StringUtils;
class VoiceAnnouncementUtils {
@@ -160,9 +160,9 @@ class VoiceAnnouncementUtils {
}
}
Pair<HeartRate, String> heartrate = sensorDataSet.getHeartRate();
if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.first != null && heartrate.first.isValid()) { //TODO Check has an announcable value?
int averageHeartRate = Math.round(heartrate.first.getBPM());
SensorData<HeartRate> heartrate = sensorDataSet.heartRate();
if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.data().isValid()) { //TODO Check has an announceable value?
int averageHeartRate = Math.round(heartrate.data().getBPM());
builder.append(" ")
.append(context.getString(R.string.current_heart_rate));
@@ -19,22 +19,22 @@ public class AltitudeCorrectionManager {
private static final String TAG = AltitudeCorrectionManager.class.getSimpleName();
public void correctAltitude(Context context, TrackPoint trackPoint) {
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
return;
public TrackPoint correctAltitude(Context context, TrackPoint trackPoint) {
if (!trackPoint.position().hasLocation() || !trackPoint.position().hasAltitude()) {
return trackPoint;
}
// TODO The following is doing IO and should not be done in main thread.
final TrackPoint[] newTrackPoint = {null}; //TODO this is kind of ugly
// AltitudeConverterCompat uses internally a RoomDatabase that cannot be access from main thread and thus fails on version <= 34.
Thread t = new Thread(() -> {
try {
Location loc = trackPoint.getPosition().toLocation();
AltitudeConverterCompat.addMslAltitudeToLocation(context, loc);
trackPoint.setAltitude(Altitude.EGM2008.of(LocationCompat.getMslAltitudeMeters(loc)));
} catch (IOException e) {
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
}
});
try {
Location loc = trackPoint.position().toLocation();
AltitudeConverterCompat.addMslAltitudeToLocation(context, loc);
newTrackPoint[0] = TrackPoint.with(trackPoint, Altitude.EGM2008.of(LocationCompat.getMslAltitudeMeters(loc)));
} catch (IOException e) {
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
}
});
t.start();
try {
@@ -42,5 +42,7 @@ public class AltitudeCorrectionManager {
} catch (InterruptedException e) {
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
}
return newTrackPoint[0];
}
}
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
@@ -15,6 +14,7 @@ import java.time.Instant;
import java.time.ZoneId;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.sensors.SensorManager;
@@ -55,15 +55,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
sensorManager.reset();
}
private SensorDataSet addSensorData(TrackPoint trackPoint) {
if (!isStarted()) {
Log.w(TAG, "Not started, should not be called.");
return null;
}
return sensorManager.fill(trackPoint);
}
public void stop() {
sensorManager.stop(context);
this.context = null;
@@ -72,13 +63,8 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
/**
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
*/
public synchronized void onChange(@NonNull SensorDataSet unused) {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow()));
}
@VisibleForTesting
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
addSensorData(trackPoint);
public synchronized void onChange() {
TrackPoint trackPoint = createTrackPoint(TrackPoint.Type.TRACKPOINT);
boolean stored = service.newTrackPoint(trackPoint, PreferencesUtils.getThresholdHorizontalAccuracy()); //TODO Cache preference for performance
if (stored) {
@@ -91,40 +77,75 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
public synchronized TrackPoint createSegmentEnd() {
TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow());
addSensorData(segmentEnd);
TrackPoint segmentEnd = createTrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
reset();
return segmentEnd;
}
public synchronized TrackPoint createIdle() {
TrackPoint idle = new TrackPoint(TrackPoint.Type.IDLE, createNow());
addSensorData(idle);
TrackPoint idle = createTrackPoint(TrackPoint.Type.IDLE);
reset();
return idle;
}
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastTrackPointUISpeed, @Nullable TrackPoint lastTrackPointUIAltitude, @Nullable TrackPoint lastStoredTrackPointWithLocation) {
TrackPoint currentTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow());
Instant now = createNow();
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
SensorDataSet sensorDataSet = addSensorData(currentTrackPoint);
Position position = sensorDataSet.position().data();
if (!currentTrackPoint.hasLocation() && lastStoredTrackPointWithLocation != null && lastStoredTrackPointWithLocation.hasLocation()) {
if (!position.hasLocation()
&& lastStoredTrackPointWithLocation != null
&& lastStoredTrackPointWithLocation.position().hasLocation()) {
//We are taking the coordinates from the last stored TrackPoint, so the distance is monotonously increasing.
currentTrackPoint.setPosition(lastStoredTrackPointWithLocation.getPosition());
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
}
if (lastTrackPointUISpeed != null) {
currentTrackPoint.setSpeed(lastTrackPointUISpeed.getSpeed());
}
if (lastTrackPointUISpeed != null)
position.with(lastTrackPointUISpeed.position().speed());
if (lastTrackPointUIAltitude != null) {
currentTrackPoint.setAltitude(lastTrackPointUIAltitude.getAltitude());
}
if (lastTrackPointUIAltitude != null)
position.with(lastTrackPointUIAltitude.position().altitude());
TrackPoint currentTrackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
position,
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
sensorDataSet.temperature() != null ? sensorDataSet.temperature().data() : null,
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
);
return new Pair<>(currentTrackPoint, sensorDataSet);
}
private TrackPoint createTrackPoint(TrackPoint.Type type) {
Instant now = createNow();
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
Position position = sensorDataSet.position().data();
if (sensorDataSet.speed() != null) {
position = position.with(sensorDataSet.speed().data());
}
return
new TrackPoint(
null,
type,
position,
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
sensorDataSet.temperature() != null ? sensorDataSet.temperature().data() : null,
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
);
}
public Instant createNow() {
return Instant.now(clock);
}
@@ -56,7 +56,7 @@ class PreferencesOpenHelper {
}
if (!parts.get(1).matches("\\d+")) {
parts.add(1, String.valueOf(PreferencesUtils.getLayoutColumnsByDefault()));
parts.add(1, String.valueOf(2));
}
PreferencesUtils.setString(R.string.stats_custom_layouts_key, String.join(CsvLayoutUtils.ITEM_SEPARATOR, parts));
}
@@ -16,17 +16,16 @@
package de.dennisguse.opentracks.settings;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
@@ -34,12 +33,11 @@ import androidx.preference.PreferenceManager;
import com.google.android.material.color.DynamicColors;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
@@ -51,7 +49,7 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.sensors.SensorType;
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
@@ -274,6 +272,10 @@ public class PreferencesUtils {
return getString(R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothTemperatureSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_temperature_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothCyclingCadenceSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone());
}
@@ -804,71 +806,54 @@ public class PreferencesUtils {
return getDefaultExportDirectoryUri() != null;
}
public static int getLayoutColumnsByDefault() {
return resources.getInteger(R.integer.stats_custom_layout_columns_default);
}
public static RecordingLayout getDefaultLayout() {
//TODO We could merge this with Mapping, right?
List<DataField> defaultFields = List.of(
new DataField(resources.getString(R.string.stats_custom_layout_distance_key), true, true, false),
new DataField(resources.getString(R.string.stats_custom_layout_total_time_key), true, true, false),
new DataField(resources.getString(R.string.stats_custom_layout_speed_key), true, true, false),
new DataField(resources.getString(R.string.stats_custom_layout_moving_time_key), true, true, false),
new DataField(resources.getString(R.string.stats_custom_layout_average_speed_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_max_speed_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_average_moving_speed_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_altitude_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_gain_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_loss_key), true, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_pace_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_average_moving_pace_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_average_pace_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_average_segment_pace_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_elapsed_segment_time_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_fastest_pace_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_coordinates_key), false, false, true),
new DataField(resources.getString(R.string.stats_custom_layout_heart_rate_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_cadence_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_power_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_clock_key), false, false, false),
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false)
);
private static List<TypedArray> getLayoutDataFields() {
return Stream.of(
R.array.stats_custom_layout_fields_default_value_0,
R.array.stats_custom_layout_fields_default_value_1,
R.array.stats_custom_layout_fields_default_value_2,
R.array.stats_custom_layout_fields_default_value_3,
R.array.stats_custom_layout_fields_default_value_4,
R.array.stats_custom_layout_fields_default_value_5,
R.array.stats_custom_layout_fields_default_value_6,
R.array.stats_custom_layout_fields_default_value_7,
R.array.stats_custom_layout_fields_default_value_8,
R.array.stats_custom_layout_fields_default_value_9,
R.array.stats_custom_layout_fields_default_value_10,
R.array.stats_custom_layout_fields_default_value_11,
R.array.stats_custom_layout_fields_default_value_12,
R.array.stats_custom_layout_fields_default_value_13,
R.array.stats_custom_layout_fields_default_value_14,
R.array.stats_custom_layout_fields_default_value_15,
R.array.stats_custom_layout_fields_default_value_16,
R.array.stats_custom_layout_fields_default_value_17,
R.array.stats_custom_layout_fields_default_value_18,
R.array.stats_custom_layout_fields_default_value_19,
R.array.stats_custom_layout_fields_default_value_20
).map(id -> resources.obtainTypedArray(id))
.toList();
}
@SuppressLint("ResourceType")
private static String buildDefaultFields() {
List<TypedArray> fieldsArrays = getLayoutDataFields();
return fieldsArrays.stream().map(i -> i.getString(0) + CsvLayoutUtils.PROPERTY_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR))
+ CsvLayoutUtils.ITEM_SEPARATOR;
RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2);
defaultLayout.addFields(defaultFields);
return defaultLayout;
}
static String buildDefaultLayout() {
return resources.getString(R.string.stats_custom_layout_default_layout) + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields();
return RecordingLayoutIO.toCSV(List.of(getDefaultLayout()));
}
public static String getDefaultLayoutName() {
return resources.getString(R.string.stats_custom_layout_default_layout);
}
/**
* @return custom layout selected or the first one if any has been selected or the one selected is not exists anymore.
*/
public static RecordingLayout getCustomLayout() {
String selectedLayout = getSelectedLayoutName();
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayouts.split(CsvLayoutUtils.LINE_SEPARATOR);
String layoutSelected = getString(R.string.stats_custom_layout_selected_layout_key, null);
if (layoutSelected == null) {
return RecordingLayoutIO.fromCsv(csvLines[0], resources);
}
for (String line : csvLines) {
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(line, resources);
if (recordingLayout.sameName(layoutSelected)) {
return recordingLayout;
}
}
List<RecordingLayout> recordingLayouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
return RecordingLayoutIO.fromCsv(csvLines[0], resources);
RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout();
return recordingLayouts.stream()
.filter(it -> it.sameName(selectedLayout))
.findFirst()
.orElse(fallback);
}
public static void updateCustomLayouts(@NonNull List<RecordingLayout> recordingLayouts) {
@@ -886,26 +871,28 @@ public class PreferencesUtils {
}
public static void addCustomLayout(@NonNull String layoutName) {
String newLayoutCsv = layoutName + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields();
String customLayoutCsv = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()) + CsvLayoutUtils.LINE_SEPARATOR + newLayoutCsv;
setString(R.string.stats_custom_layouts_key, customLayoutCsv);
ArrayList<RecordingLayout> currentLayouts = new ArrayList<>(PreferencesUtils.getAllCustomLayouts());
RecordingLayout newCustomLayout = RecordingLayout.fromDefault(layoutName);
currentLayouts.add(newCustomLayout);
setString(R.string.stats_custom_layouts_key, RecordingLayoutIO.toCSV(currentLayouts));
}
public static void setDefaultLayout(String layoutName) {
@Nullable
public static String getSelectedLayoutName() {
return getString(R.string.stats_custom_layout_selected_layout_key, null);
}
public static void setSelectedLayoutName(String layoutName) {
setString(R.string.stats_custom_layout_selected_layout_key, layoutName);
}
public static List<RecordingLayout> getAllCustomLayouts() {
String csvCustomLayout = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayout.split(CsvLayoutUtils.LINE_SEPARATOR);
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
return Arrays.stream(csvLines).map(line -> RecordingLayoutIO.fromCsv(line, resources)).toList();
}
public static List<String> getAllCustomLayoutNames() {
return getAllCustomLayouts().stream()
.map(RecordingLayout::getName)
.toList();
return RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
}
public static void resetCustomLayoutPreferences() {
@@ -7,7 +7,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
@@ -29,6 +29,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerManagerCyclingPower.CYCLING_POWER));
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerCyclingPower.CYCLING_POWER));
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerHeartRate;
import de.dennisguse.opentracks.sensors.BluetoothHandlerHeartRate;
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerHeartRate.HEART_RATE_SUPPORTING_DEVICES);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerHeartRate.HEART_RATE_SUPPORTING_DEVICES);
}
}
@@ -0,0 +1,35 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerTemperature;
public class BluetoothLeTemperaturePreference extends BluetoothLeSensorPreference {
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeTemperaturePreference(Context context) {
super(context);
}
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(BluetoothHandlerTemperature.TEMPERATURE));
}
}
@@ -31,7 +31,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
public class TrackListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ActionMode.Callback {
private static final String TAG = TrackListAdapter.class.getSimpleName();
private final AppCompatActivity context;
private final RecyclerView recyclerView;
@@ -193,19 +192,25 @@ public class TrackListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
viewBinding.trackListItemName.setText(tracksListItem.name());
String timeDistanceText = ListItemUtils.getTimeDistanceText(context, unitSystem, isRecordingThisTrackRecording, tracksListItem.totalTime(), tracksListItem.totalDistance(), tracksListItem.markerCount());
viewBinding.trackListItemTimeDistance.setText(timeDistanceText);
String timeText = ListItemUtils.getTimeText(context, isRecordingThisTrackRecording, tracksListItem.totalTime());
String distanceText = ListItemUtils.getDistanceText(context, unitSystem, isRecordingThisTrackRecording, tracksListItem.totalDistance());
viewBinding.trackListItemMarkerCountIcon.setVisibility(tracksListItem.markerCount() > 0 ? View.VISIBLE : View.GONE);
viewBinding.trackListItemMarkerCount.setText(tracksListItem.markerCount() > 0 ? Integer.toString(tracksListItem.markerCount()) : null);
viewBinding.trackListItemDistance.setText(distanceText);
viewBinding.trackListItemDuration.setText(timeText);
if (!recordingStatus.isRecording()) {
ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, tracksListItem.startTime(), tracksListItem.zoneOffset());
if (tracksListItem.markerCount() > 0) {
viewBinding.trackListItemDistanceMarkerSeparator.setVisibility(View.VISIBLE);
viewBinding.trackListItemMarkerCountIcon.setVisibility(View.VISIBLE);
viewBinding.trackListItemMarkerCount.setText(Integer.toString(tracksListItem.markerCount()));
} else {
viewBinding.trackListItemDate.setText(null);
viewBinding.trackListItemTime.setText(null);
viewBinding.trackListItemDistanceMarkerSeparator.setVisibility(View.GONE);
viewBinding.trackListItemMarkerCountIcon.setVisibility(View.GONE);
viewBinding.trackListItemMarkerCount.setText(null);
}
ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, tracksListItem.startTime(), tracksListItem.zoneOffset());
viewBinding.trackListItemActivityTypeLocalized.setText(tracksListItem.activityTypeLocalized());
viewBinding.trackListItemActivityTypeLocalized.setVisibility(TextUtils.isEmpty(tracksListItem.activityTypeLocalized()) ? View.GONE : View.VISIBLE);
@@ -5,13 +5,26 @@ import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
//TODO refactor; rename ot CsvUtils
public final class CsvLayoutUtils {
@Deprecated //TODO private | package private
public static final String LINE_SEPARATOR = "\n";
@Deprecated //TODO private | package private
public static final String ITEM_SEPARATOR = ";";
@Deprecated //TODO private | package private
public static final String PROPERTY_SEPARATOR = ",";
private CsvLayoutUtils() {
}
@Nullable
public static List<String> getCsvLines(String csv) {
if (csv == null) {
return null;
}
return Arrays.stream(csv.split(CsvLayoutUtils.LINE_SEPARATOR)).toList();
}
/**
@@ -17,9 +17,18 @@ public class RecordingLayout implements Parcelable {
private int columnsPerRow;
private final List<DataField> dataFields = new ArrayList<>();
public static RecordingLayout fromDefault(String newName) {
RecordingLayout defaultLayout = PreferencesUtils.getDefaultLayout();
RecordingLayout recordingLayout = new RecordingLayout(newName, defaultLayout.getColumnsPerRow());
recordingLayout.addFields(defaultLayout.getFields());
return recordingLayout;
}
@Deprecated //TODO please set columnsPerRow explicitly
public RecordingLayout(String name) {
this.name = name;
this.columnsPerRow = PreferencesUtils.getLayoutColumnsByDefault();
this.columnsPerRow = PreferencesUtils.getDefaultLayout().getColumnsPerRow();
}
public RecordingLayout(String name, int columnsPerRow) {
@@ -90,11 +99,11 @@ public class RecordingLayout implements Parcelable {
}
public boolean sameName(RecordingLayout recordingLayout) {
return this.name.equalsIgnoreCase(recordingLayout.getName());
return sameName(recordingLayout.getName());
}
public boolean sameName(String name) {
return this.name.equalsIgnoreCase(name);
return this.name.equals(name);
}
public String toCsv() {
@@ -19,11 +19,17 @@ public class RecordingLayoutIO {
private static final String YES_VALUE = "1";
private static final String NOT_VALUE = "0";
public static RecordingLayout fromCsv(@NonNull String csvLine, @NonNull Resources resources) {
public static List<RecordingLayout> fromCsv(@NonNull String csv, @NonNull Resources resources) {
return CsvLayoutUtils.getCsvLines(csv).stream()
.map(line -> RecordingLayoutIO.fromCsvLine(line, resources))
.toList();
}
public static RecordingLayout fromCsvLine(@NonNull String csvLine, @NonNull Resources resources) {
List<String> csvParts = CsvLayoutUtils.getCsvLineParts(csvLine);
if (csvParts == null) {
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
return new RecordingLayout(PreferencesUtils.getDefaultLayoutName());
return PreferencesUtils.getDefaultLayout();
}
RecordingLayout recordingLayout = new RecordingLayout(csvParts.get(0), Integer.parseInt(csvParts.get(1)));
@@ -76,7 +76,7 @@ public class IntervalStatisticsUpdater {
}
}
return trackPoint != null ? trackPoint.getId() : null;
return trackPoint != null ? trackPoint.id() : null;
}
public List<Statistics> getIntervalList() {
@@ -152,9 +152,9 @@ public class IntervalStatisticsUpdater {
if (lastTrackPoint == null) {
return;
}
if (gain_m != null && lastTrackPoint.hasAltitudeGainLoss()) {
gain_m = gain_m - lastTrackPoint.getAltitudeGainLoss().gain_m();
loss_m = loss_m - lastTrackPoint.getAltitudeGainLoss().loss_m();
if (gain_m != null && lastTrackPoint.altitudeGainLoss() != null) {
gain_m = gain_m - lastTrackPoint.altitudeGainLoss().gain_m();
loss_m = loss_m - lastTrackPoint.altitudeGainLoss().loss_m();
}
}
@@ -150,7 +150,7 @@ public class MarkerEditViewModel extends AndroidViewModel {
name,
"",
"",
trackPoint.getPosition(),
trackPoint.position(),
null);
if (markerData == null) {
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.ui.util;
import android.content.Context;
import android.text.TextUtils;
import android.widget.TextView;
import java.time.Duration;
@@ -42,50 +41,30 @@ public class ListItemUtils {
private ListItemUtils() {
}
/**
* Gets a string for share owner, total time, and total distance.
*
* @param totalTime the total time. Can be null
* @param totalDistance the total distance. Can be null
*/
private static String getTimeDistance(String totalTime, String totalDistance) {
StringBuilder builder = new StringBuilder();
if (!TextUtils.isEmpty(totalTime)) {
if (builder.length() != 0) {
builder.append(" ‧ ");
}
builder.append(totalTime);
public static String getTimeText(Context context, boolean isRecording, Duration totalTime) {
String timeText;
if (isRecording) {
timeText = context.getString(R.string.generic_recording);
} else {
timeText = StringUtils.formatElapsedTime(totalTime);
}
if (!TextUtils.isEmpty(totalDistance)) {
if (builder.length() != 0) {
builder.append(" ");
}
//TODO Use strings.xml
builder.append("(").append(totalDistance).append(")");
}
return builder.toString();
return timeText;
}
public static String getTimeDistanceText(Context context, UnitSystem unitSystem, boolean isRecording, Duration totalTime, Distance totalDistance, int markerCount) {
String timeDistanceText;
if (isRecording) {
timeDistanceText = context.getString(R.string.generic_recording);
} else {
// Match list_item_time_distance in list_item.xml
public static String getDistanceText(Context context, UnitSystem unitSystem, boolean isRecording, Distance totalDistance) {
String time = StringUtils.formatElapsedTime(totalTime);
String distance = DistanceFormatter.Builder()
StringBuilder builder = new StringBuilder();
if (!isRecording) {
String distanceText = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context)
.formatDistance(totalDistance);
timeDistanceText = getTimeDistance(time, distance);
if (markerCount > 0) {
timeDistanceText += " ‧";
}
builder.append("(").append(distanceText).append(")");
}
return timeDistanceText;
return builder.toString();
}
public static void setDateAndTime(Context context, TextView dateView, TextView timeView, Instant time, ZoneOffset timeZone) {
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.util;
import static java.time.temporal.ChronoUnit.DAYS;
import android.content.Context;
import android.location.Location;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
@@ -47,6 +46,7 @@ import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
@@ -156,6 +156,15 @@ public class StringUtils {
return new Pair<>(value, context.getString(R.string.sensor_unit_beats_per_minute));
}
public static Pair<String, String> getTemperatureParts(Context context, Temperature temperature) {
String value = context.getString(R.string.value_unknown);
if (temperature != null) {
value = StringUtils.formatDecimal(temperature.getCelsius(), 1);
}
return new Pair<>(value, context.getString(R.string.sensor_unit_temperature));
}
public static Pair<String, String> getCadenceParts(Context context, Cadence cadence) {
String value = context.getString(R.string.value_unknown);
if (cadence != null) {
@@ -84,11 +84,11 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
SensorDataSet sensorDataSet = data.sensorDataSet();
final TrackPoint latestTrackPoint = data.latestTrackPoint();
if (sensorDataSet != null && sensorDataSet.getSpeed() != null) {
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.getSpeed().first);
getBinding().statsDescriptionMain.setText(sensorDataSet.getSpeed().second);
if (sensorDataSet != null && sensorDataSet.speed() != null) {
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.speed().data());
getBinding().statsDescriptionMain.setText(sensorDataSet.speed().sensorNameOrAddress());
} else {
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
Speed speed = latestTrackPoint != null && latestTrackPoint.position().hasSpeed() ? latestTrackPoint.position().speed() : null;
valueAndUnit = localSpeedFormatter.getSpeedParts(speed);
String title = reportSpeed ? getContext().getString(R.string.stats_speed) : getContext().getString(R.string.stats_pace);
@@ -249,8 +249,8 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
@Override
public void onChanged(UnitSystem unitSystem, RecordingData data) {
TrackPoint latestTrackPoint = data.latestTrackPoint();
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
String altitudeReference = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? getContext().getString(latestTrackPoint.getAltitude().getLabelId()) : null;
Float altitude = latestTrackPoint != null && latestTrackPoint.position().hasAltitude() ? (float) latestTrackPoint.position().altitude().toM() : null;
String altitudeReference = latestTrackPoint != null && latestTrackPoint.position().hasAltitude() ? getContext().getString(latestTrackPoint.position().altitude().getLabelId()) : null;
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), altitude, unitSystem);
getBinding().statsValue.setText(valueAndUnit.first);
@@ -294,8 +294,8 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
public void onChanged(UnitSystem unitSystem, RecordingData data) {
TrackPoint latestTrackPoint = data.latestTrackPoint();
String value;
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
value = StringUtils.formatCoordinate(getContext(), latestTrackPoint.getPosition());
if (latestTrackPoint != null && latestTrackPoint.position().hasLocation()) {
value = StringUtils.formatCoordinate(getContext(), latestTrackPoint.position());
} else {
value = getContext().getString(R.string.value_unknown);
}
@@ -34,6 +34,7 @@ public class Mapping {
m.put(context.getString(R.string.stats_custom_layout_coordinates_key), GenericStatisticsViewHolder.Coordinates::new);
m.put(context.getString(R.string.stats_custom_layout_heart_rate_key), SensorStatisticsViewHolder.SensorHeartRate::new);
m.put(context.getString(R.string.stats_custom_layout_temperature_key), SensorStatisticsViewHolder.SensorTemperature::new);
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
@@ -3,10 +3,7 @@ package de.dennisguse.opentracks.viewmodels;
import android.util.Pair;
import android.view.LayoutInflater;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.HeartRateZones;
import de.dennisguse.opentracks.databinding.StatsSensorItemBinding;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
@@ -37,10 +34,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
String sensorName = getContext().getString(R.string.value_unknown);
Pair<String, String> valueAndUnit;
Pair<HeartRate, String> heartRate;
if (sensorDataSet != null && (heartRate = sensorDataSet.getHeartRate()) != null) {
valueAndUnit = StringUtils.getHeartRateParts(getContext(), heartRate.first);
sensorName = heartRate.second;
if (sensorDataSet != null && sensorDataSet.heartRate() != null) {
valueAndUnit = StringUtils.getHeartRateParts(getContext(), sensorDataSet.heartRate().data());
sensorName = sensorDataSet.heartRate().sensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getHeartRateParts(getContext(), null);
}
@@ -48,8 +44,8 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
//TODO Loads preference every time
HeartRateZones zones = PreferencesUtils.getHeartRateZones();
int textColor;
if (sensorDataSet != null && (heartRate = sensorDataSet.getHeartRate()) != null) {
textColor = zones.getTextColorForZone(getContext(), heartRate.first);
if (sensorDataSet != null && sensorDataSet.heartRate() != null) {
textColor = zones.getTextColorForZone(getContext(), sensorDataSet.heartRate().data());
} else {
textColor = zones.getTextColorForZone(getContext(), null);
}
@@ -64,6 +60,29 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
}
}
public static class SensorTemperature extends SensorStatisticsViewHolder {
@Override
public void onChanged(UnitSystem unitSystem, RecordingData data) {
SensorDataSet sensorDataSet = data.sensorDataSet();
String sensorName = getContext().getString(R.string.value_unknown);
Pair<String, String> valueAndUnit;
if (sensorDataSet != null && sensorDataSet.temperature() != null) {
valueAndUnit = StringUtils.getTemperatureParts(getContext(), sensorDataSet.temperature().data());
sensorName = sensorDataSet.temperature().sensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getTemperatureParts(getContext(), null);
}
getBinding().statsValue.setText(valueAndUnit.first);
getBinding().statsUnit.setText(valueAndUnit.second);
getBinding().statsDescriptionMain.setText(R.string.stats_sensors_temperature);
getBinding().statsDescriptionSecondary.setText(sensorName);
}
}
public static class SensorCadence extends SensorStatisticsViewHolder {
@Override
@@ -72,9 +91,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
String sensorName = getContext().getString(R.string.value_unknown);
Pair<String, String> valueAndUnit;
if (sensorDataSet != null && sensorDataSet.getCadence() != null) {
valueAndUnit = StringUtils.getCadenceParts(getContext(), sensorDataSet.getCadence().first);
sensorName = sensorDataSet.getCadence().second;
if (sensorDataSet != null && sensorDataSet.cadence() != null) {
valueAndUnit = StringUtils.getCadenceParts(getContext(), sensorDataSet.cadence().data());
sensorName = sensorDataSet.cadence().sensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getCadenceParts(getContext(), null);
}
@@ -95,9 +114,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
String sensorName = getContext().getString(R.string.value_unknown);
Pair<String, String> valueAndUnit;
if (sensorDataSet != null && sensorDataSet.getCyclingPower() != null) {
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.getCyclingPower().getAggregatedValue(Instant.now())); //TODO Use MonotonicClock
sensorName = sensorDataSet.getCyclingPower().getSensorNameOrAddress();
if (sensorDataSet != null && sensorDataSet.power() != null) {
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.power().data());
sensorName = sensorDataSet.power().sensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getCadenceParts(getContext(), null);
}
+3 -3
View File
@@ -54,12 +54,12 @@ limitations under the License.
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
app:fabAlignmentMode="end"
app:fabAlignmentMode="center"
app:fabAnchorMode="cradle">
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.IconButton"
android:id="@+id/aggregated_stats_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_statistics_24dp"
@@ -77,11 +77,11 @@ limitations under the License.
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/track_list_fab_action"
android:tint="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_baseline_record_24"
android:tint="@android:color/white"
app:layout_anchor="@id/bottom_app_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+44 -13
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/activated_background"
android:paddingTop="8dp"
android:paddingHorizontal="8dp">
android:paddingHorizontal="8dp"
android:paddingTop="8dp">
<!-- Icon -->
<ImageView
@@ -27,8 +27,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:singleLine="true"
app:layout_constraintStart_toEndOf="@id/track_list_item_icon"
app:layout_constraintEnd_toStartOf="@id/track_list_item_date"
app:layout_constraintStart_toEndOf="@id/track_list_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Main Text" />
@@ -44,14 +44,41 @@
tools:text="2022-02-17" />
<!-- Total time and distance -->
<TextView
android:id="@+id/track_list_item_time_distance"
style="?attr/textAppearanceBodySmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
<LinearLayout
android:id="@+id/track_list_item_duration_distance_wrapper"
android:layout_width="150dp"
android:layout_height="18dp"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="@id/track_list_item_name"
app:layout_constraintTop_toBottomOf="@id/track_list_item_name"
tools:text="50:43 (50.00km)" />
app:layout_constraintTop_toBottomOf="@id/track_list_item_name">
<TextView
android:id="@+id/track_list_item_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="012:50:43" />
<TextView
android:id="@+id/track_list_item_distance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="(50.00km)"
android:textAlignment="textEnd" />
</LinearLayout>
<TextView
android:id="@+id/track_list_item_distance_marker_separator"
android:layout_width="10dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text=" ‧"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="@id/track_list_item_duration_distance_wrapper"
app:layout_constraintStart_toEndOf="@id/track_list_item_duration_distance_wrapper"
app:layout_constraintTop_toBottomOf="@id/track_list_item_name" />
<!-- Marker Count Icon -->
<ImageView
@@ -61,8 +88,8 @@
android:contentDescription="@string/image_marker_count"
android:scaleType="centerInside"
android:src="@drawable/ic_marker_show_24dp"
app:layout_constraintBottom_toBottomOf="@id/track_list_item_time_distance"
app:layout_constraintStart_toEndOf="@id/track_list_item_time_distance"
app:layout_constraintBottom_toBottomOf="@+id/track_list_item_distance_marker_separator"
app:layout_constraintStart_toEndOf="@+id/track_list_item_distance_marker_separator"
app:layout_constraintTop_toBottomOf="@id/track_list_item_name" />
<!-- Marker count -->
@@ -93,7 +120,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="track_list_item_time_distance,track_list_item_marker_count_icon,track_list_item_marker_count,track_list_item_time" />
app:constraint_referenced_ids="track_list_item_duration_distance_wrapper,track_list_item_marker_count_icon,track_list_item_marker_count,track_list_item_time" />
<!-- Category and description -->
<TextView
@@ -129,4 +156,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/track_list_item_description" />
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
+10 -10
View File
@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_detail_activity_tablayout"
@@ -19,9 +19,9 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/track_recording_coordinator_layout"
android:layout_width="match_parent"
android:paddingBottom="?attr/actionBarSize"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:paddingBottom="?attr/actionBarSize">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/track_detail_activity_view_pager"
@@ -37,17 +37,17 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
app:fabAlignmentMode="end"
app:fabAlignmentMode="center"
app:fabAnchorMode="cradle"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:menu="@menu/track_record" />
app:menu="@menu/track_record"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/track_recording_fab_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_record_24"
android:contentDescription="@string/image_stop"
android:src="@drawable/ic_baseline_record_24"
app:layout_anchor="@id/bottom_app_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+6 -6
View File
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="activity_type_airplane">aeroplane</string>
<string name="activity_type_atv">ATV 4-Wheeler</string>
<string name="activity_type_biking">A bike ride</string>
<string name="activity_type_atv">ATV</string>
<string name="activity_type_biking">biking</string>
<string name="activity_type_blimp">airship</string>
<string name="activity_type_boat">Boating</string>
<string name="activity_type_boat">boat</string>
<string name="activity_type_climbing">climbing</string>
<string name="activity_type_commercial_airplane">commercial flight</string>
<string name="activity_type_commercial_airplane">commercial aeroplane</string>
<string name="activity_type_cross_country_skiing">Cross-country skiing</string>
<string name="activity_type_cycling">cycling</string>
<string name="activity_type_dirt_bike">dirt bike</string>
<string name="activity_type_donkey_back_riding">Donkey ride</string>
<string name="activity_type_donkey_back_riding">donkey riding</string>
<string name="activity_type_driving">driving</string>
<string name="activity_type_driving_bus">driving bus</string>
<string name="activity_type_driving_car">Car driving</string>
<string name="activity_type_driving_car">driving car</string>
<string name="activity_type_ferry">ferry</string>
<string name="activity_type_frisbee">frisbee</string>
<string name="activity_type_gliding">gliding</string>
+254 -160
View File
@@ -15,59 +15,38 @@ See the License for the specific language governing permissions and
limitations under the License.
--><resources>
<string name="about_preference_title">Teave OpenTracks\'i kohta</string>
<string name="about_description"><i>OpenTracks</i> on spordi jälgimise rakendus, mis austab teie privaatsust.</string>
<string name="about_description"><i>OpenTracks</i> on sinu sportimise jälgimise rakendus, mis austab sinu privaatsust.</string>
<string name="about_url">Projekti veebileht: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Versiooni nimi: %1$s</string>
<string name="about_version_code">Versiooni kood: %1$d</string>
<string name="about_history">OpenTracks on <a href="https://code.google.com/archive/p/mytracks/"> <i>Google\'i MyTracks</i> </a> haru (seisatud 2016. aastal).</string>
<string name="about_history">OpenTracks on <a href="https://code.google.com/archive/p/mytracks/"> <i>Google\'i MyTracks</i> </a> haru (arendamine lõppes 2016. aastal).</string>
<string name="about_libraries">Teegid</string>
<!-- Help -->
<string name="help_feature_title">OpenTracks\'il on järgmised funktsioonid:</string>
<string name="help_feature_content">* loo GPS radu
\n* kasutage pulsi andurit (ainult Bluetooth LE)
\n* häälteated (aja või vahemaa järgi)
\n* loo asukohapunkte (koos piltidega)
\n* töötab ilma internetiühenduseta
\n* näita radu kaardil (vt allapoole)</string>
<string name="help_track_map_title">Kuidas näidata rada kaardil\?</string>
<string name="help_track_map_content"> Kasutage väikest kaardisümbolit.
\n a) (reaalajas uuendused) Installige <a href="https://codeberg.org/OpenTracksApp/OSMDashboard">OSMDashboard</a>. See pakub online või offline kaarte <a href="https://www.openstreetmap.org">OpenStreetMap</a> kaudu.
\n Tegelikult, OSMDashboard on eraldi rakendus, mis saab juurdepääsu (ajutiselt) ainult näidatavate rada(de) andmetele.
\n b) Tavalise kaardirakenduse kaudu (nt <a href="http://osmand.net/">OsmAnd</a> ja <a href="https://maps.me/">MAPS.ME</a>). Need rakendused peavad toetama KMZ failivormingut (ainult asukohad ja ajatemplid). Tegelikult peab rakendus toetama <i>ACTION_VIEW</i>-d koos MIME <i>application/vnd.google-earth.kmz</i>-ga. </string>
<string name="help_feature_title">OpenTracks\'il on järgmised funktsionaalsused:</string>
<string name="help_feature_content">* loo GPS radu \n* kasuta pulsiandurit (ainult Bluetooth LE) \n* häälteated (aja või vahemaa järgi) \n* loo asukohapunkte (koos piltidega) \n* töötab ilma internetiühenduseta \n* vaata radu kaardil (vt allapoole)</string>
<string name="help_track_map_title">Kuidas toimib raja näitamine kaardil?</string>
<string name="help_track_map_content">Kasuta väikest kaardisümbolit. \n a) (reaalajas uuendused) Paigalda <a href="https://codeberg.org/OpenTracksApp/OSMDashboard">OSMDashboard</a>. See pakub <a href="https://www.openstreetmap.org">OpenStreetMapi</a> abil kaarte nii võrguühendusega kui ilma. \n Tegelikult, OSMDashboard on eraldi rakendus, mis saab juurdepääsu (ajutiselt) ainult näidatavate rada(de) andmetele. \n b) Tavalise kaardirakenduse kaudu (nt <a href="http://osmand.net/">OsmAnd</a> ja <a href="https://maps.me/">MAPS.ME</a>). Need rakendused peavad toetama KMZ failivormingut (ainult asukohad ja ajatemplid). Tegelikult peab rakendus toetama <i>ACTION_VIEW</i>-d koos MIME <i>application/vnd.google-earth.kmz</i>-ga.</string>
<string name="help_storage_pictures_title">Kuidas salvestab OpenTracks asukohapunktitena tehtud pilte\?</string>
<string name="help_storage_pictures_content">Neid hoitakse rakenduse sisemises salvestusruumis (st ligipääsetav ainult Opentracks\'i siseselt).</string>
<string name="help_track_export_title">Kuidas saab salvestatud radu eksportida/jagada\?</string>
<string name="help_track_export_content"> Radu saab seadetest eksportida KMZ-, KML- või GPX-vormingus. KMZ/KML kõik andmed; GPX ainult asukohad. Radasid, mis on jagatud <i>jagamise ikooni</i> kaudu, jagatakse KMZ-na. </string>
<string name="help_data_google_play_services_title">Kas OpenTracks nõuab/kasutab Google Play teenust\?</string>
<string name="help_recording_title">Kuidas salvestab OpenTracks andmeid\?</string>
<string name="help_track_export_content">Radu saab seadetest eksportida KMZ-, KML- või GPX-vormingus. KMZ/KML kõik andmed; GPX ainult asukohad. Radasid, mis on jagatud <i>jagamise ikooni</i> kaudu, jagatakse KMZ-formingus.</string>
<string name="help_data_google_play_services_title">Kas OpenTracks eeldab/kasutab Google Play teenust?</string>
<string name="help_recording_title">Kuidas OpenTracks salvestab andmeid?</string>
<string name="help_recording_content">OpenTracks kasutab seadme GPS-andurit pidevalt asukoha salvestamiseks. Kui GPS-seade tagastab ebatäpseid andmeid (nt asukoht, kiirus, tõus), ei saa OpenTracks sellega midagi teha.</string>
<string name="help_data_storage_title">Kuidas OpenTracks sensori andmeid salvestab?</string>
<string name="help_data_storage_content"> Sensori andmed salvestatakse iga asukoha kohta. Pulsi anduri puhul tähendab see, et iga asukoha kohta salvestatakse ainult <i>üks</i> pulsi mõõtmine. Kui seisate paigal või siseruumides, siis anduri andmeid ei salvestata (kuid neid näidatakse). </string>
<string name="help_gps_troubleshooting_title">OpenTracks ei salvesta asukohti?</string>
<string name="help_gps_troubleshooting_content"> Mida saate teha:
\n a) kontrollige, kas GPS on sisse lülitatud,
\n b) paranda GPS-i vastuvõttu (nt minge õue),
\n c) testige <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> rakendusega ja
\n d) lülita energiasäästu režiim välja.
\n
\n Kui mitte midagi ei aita, palun teatage meid veast. </string>
<string name="help_data_storage_title">Kuidas OpenTracks salvestab sensori andmeid?</string>
<string name="help_data_storage_content">Sensori andmed salvestatakse iga asukoha kohta. Pulsianduri puhul tähendab see, et iga asukoha kohta salvestatakse ainult <i>üks</i> pulsimõõtmine. Kui seisad paigal või siseruumides, siis anduri andmeid ei salvestata (kuid neid näidatakse).</string>
<string name="help_gps_troubleshooting_title">Kas OpenTracks ei salvesta asukohti?</string>
<string name="help_gps_troubleshooting_content">Mida saad teha: \n a) kontrolli, kas GPS on sisse lülitatud, \n b) paranda GPS-i vastuvõttu (nt mine õue), \n c) testi <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> rakendusega ja \n d) lülita energiasäästurežiim välja. \n \n Kui mitte midagi ei aita, palun teata meile veast.</string>
<string name="help_support_title">Kuidas ma saan OpenTracks\'i toetada\?</string>
<string name="help_support_content"> Te saate:
\n a) levitada sõna OpenTracks\'i kohta,
\n b) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">ettepanekuid teha</a>,
\n c) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">teatada vigadest</a>,
\n d) aidata tarkvara arendamisel,
\n e) osta rakendus <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play poest</a>,
\n f) annetada ja
\n g) teha midagi, mis teie arvates aitaks OpenTracks\'i. </string>
<string name="help_support_content">Sa saad: \n a) levitada sõna OpenTracks\'i kohta, \n b) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">ettepanekuid teha</a>, \n c) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">teatada vigadest</a>, \n d) aidata tarkvara arendamisel, \n e) osta rakenduse <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play poest</a>, \n f) annetada ja \n g) teha mida iganes muus, mis sinu arvates aitaks OpenTracks\'i.</string>
<!-- Activity Type -->
<string name="activity_type_airplane">lennuk</string>
<string name="activity_type_atv">ATV</string>
<string name="activity_type_biking">jalgrattasõit</string>
<string name="activity_type_blimp">õhulaev</string>
<string name="activity_type_blimp">väikelennuk</string>
<string name="activity_type_boat">paat</string>
<string name="activity_type_climbing">mägironimine</string>
<string name="activity_type_commercial_airplane">kaubalennuk</string>
<string name="activity_type_commercial_airplane">reisilennuk</string>
<string name="activity_type_cross_country_skiing">murdmaasuusatamine</string>
<string name="activity_type_cycling">jalgrattasõit</string>
<string name="activity_type_dirt_bike">krossiratas</string>
@@ -95,7 +74,7 @@ limitations under the License.
<string name="activity_type_off_trail_hiking">maastikul matkamine</string>
<string name="activity_type_other">muu</string>
<string name="activity_type_paddling">sõudmine</string>
<string name="activity_type_para_gliding">motovarjuga lendamine</string>
<string name="activity_type_para_gliding">langevarjulend</string>
<string name="activity_type_rc_airplane">kaugjuhitav lennuk</string>
<string name="activity_type_rc_boat">kaugjuhitav paat</string>
<string name="activity_type_rc_helicopter">kaugjuhitav helikopter</string>
@@ -110,9 +89,9 @@ limitations under the License.
<string name="activity_type_skating">uisutamine</string>
<string name="activity_type_skiing">suusatamine</string>
<string name="activity_type_sky_jumping">langevarjuhüppamine</string>
<string name="activity_type_sled">kelk</string>
<string name="activity_type_sled">kelgutamine</string>
<string name="activity_type_snow_boarding">lumelauasõit</string>
<string name="activity_type_snow_shoeing">lumeräätsadega matkamine</string>
<string name="activity_type_snow_shoeing">räätsadega matkamine</string>
<string name="activity_type_speed_walking">kiirkõnd</string>
<string name="activity_type_street_running">linnas jooksmine</string>
<string name="activity_type_surfing">surfamine</string>
@@ -129,7 +108,7 @@ limitations under the License.
<string name="activity_type_wind_surfing">purilauasõit</string>
<!-- Description -->
<string name="description_activity_type">Tegevuse tüüp: %1$s</string>
<string name="description_average_moving_pace_in_minute">Keskmine liikumis tempo: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_moving_pace_in_minute">Keskmine liikumistempo: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_moving_speed">Keskmine liikumiskiirus: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_average_pace_in_minute">Keskmine tempo: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_speed">Keskmine kiirus: %1$.2f km/h (%2$.1f mi/h)</string>
@@ -145,71 +124,71 @@ limitations under the License.
<string name="description_pace_metric">Tempo (min/km)</string>
<string name="description_recorded_time">Salvestatud: %1$s</string>
<string name="description_speed_ms">Kiirus (m/s)</string>
<string name="description_sensor_cadence">Rütm (p/min)</string>
<string name="description_sensor_cadence">Väntamissagedus (p/min)</string>
<string name="description_sensor_heart_rate">Pulsisagedus (lööki minutis)</string>
<string name="description_sensor_power">Võimsus (W)</string>
<string name="description_default_speed_or_pace">Tegevuse tüübi järgi</string>
<string name="description_speed_imperial">Kiirus (mi/h)</string>
<string name="description_speed_metric">Kiirus (km/h)</string>
<string name="description_speed_imperial">Kiirus (mi/t)</string>
<string name="description_speed_metric">Kiirus (km/t)</string>
<string name="description_time">Aeg</string>
<string name="description_total_distance">Kogupikkus: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_distance">Kogu vahemaa: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_time">Aeg kokku: %1$s</string>
<!-- Export -->
<string name="export_with_photos">koos fotodega</string>
<string name="export_without_photos">ilma fotodeta</string>
<string name="export_progress_message">Eksportimine kataloogi %1$s…</string>
<string name="export_progress_message">Ekspordin kausta %1$s…</string>
<!-- External Storage -->
<!-- Generic -->
<string name="generic_add">Lisa</string>
<string name="generic_delete_selected_confirm_title">Kas kustutada valitud?</string>
<string name="generic_delete_selected_confirm_title">Kas kustutame valiku?</string>
<string name="generic_description">Kirjeldus</string>
<string name="generic_description_line">Kirjeldus: %1$s</string>
<string name="generic_name">Nimi</string>
<string name="generic_name_line">Nimi: %1$s</string>
<string name="generic_recording">Salvestamine…</string>
<string name="generic_recording">Salvestamisel…</string>
<string name="generic_save">Salvesta</string>
<string name="generic_tracks">Rajad</string>
<!-- Gps -->
<string name="gps_starting">GPS-i käivitamine</string>
<string name="gps_wait_for_better_signal">Oodatakse paremat GPS-signaali</string>
<string name="gps_wait_for_signal">Oodatakse GPS-signaali</string>
<string name="gps_starting">GPS on käivitamisel</string>
<string name="gps_wait_for_better_signal">Ootan paremat GPS-signaali</string>
<string name="gps_wait_for_signal">Ootan GPS-signaali</string>
<!-- Image -->
<string name="image_arrow">Nool</string>
<string name="image_marker_count">Märgistuste arv</string>
<string name="image_photo_marker">Fotomärgis</string>
<string name="image_marker_count">Markerite arv</string>
<string name="image_photo_marker">Fotomarker</string>
<string name="image_record">Salvesta</string>
<string name="image_stop">Peata</string>
<string name="image_track">Rada</string>
<!-- Import -->
<string name="import_progress_message">Importimine kataloogist %1$s…</string>
<string name="import_progress_message">Impordin kaustast %1$s…</string>
<!-- Marker -->
<string name="marker_add_error">Ei saa märgist lisada. GPS-signaal puudub. Proovige uuesti.</string>
<string name="marker_add_success">Märgis lisati</string>
<string name="marker_add_canceled">Tühistatud, märgist ei lisatud</string>
<string name="marker_add_photo_canceled">Tühistatud, fotomärgist ei lisatud</string>
<string name="marker_delete_one_confirm_message">Märgis kustutatakse seadmest jäädavalt</string>
<string name="marker_delete_one_confirm_title">Kas kustutada märgis\?</string>
<string name="marker_delete_multiple_confirm_message">Valitud märgis kustutatakse seadmest jäädavalt</string>
<string name="marker_name_format">Märgis %1$d</string>
<string name="marker_add_error">Ei saa markerit lisada. GPS-signaal puudub. Proovi uuesti.</string>
<string name="marker_add_success">Marker on lisatud</string>
<string name="marker_add_canceled">Tühistatud, marker on lisamata</string>
<string name="marker_add_photo_canceled">Tühistatud, fotomarker on lisamata</string>
<string name="marker_delete_one_confirm_message">Marker kustutatakse seadmest jäädavalt</string>
<string name="marker_delete_one_confirm_title">Kas kustutame markeri?</string>
<string name="marker_delete_multiple_confirm_message">Valitud marker kustutatakse seadmest jäädavalt</string>
<string name="marker_name_format">Marker %1$d</string>
<!-- Marker Detail -->
<!-- Marker Edit -->
<string name="marker_edit_marker_type">Märgise tüüp (nt juga)</string>
<string name="marker_edit_marker_type">Markeri tüüp (nt juga)</string>
<!-- Marker List -->
<string name="marker_list_empty_message">Teil ei ole märgiseid</string>
<string name="marker_list_empty_message">Sul ei ole markereid</string>
<!-- Menu -->
<string name="menu_aggregated_statistics">Agregeeritud statistika</string>
<string name="menu_aggregated_statistics">Koondstatistika</string>
<string name="menu_delete">Kustuta</string>
<string name="menu_edit">Muuda</string>
<string name="menu_export_all">Ekspordi kõik</string>
<string name="menu_help">Abi</string>
<string name="menu_export_all">Ekspordi kõik (üks fail raja kohta)</string>
<string name="menu_help">Abiteave</string>
<string name="menu_import_all">Impordi kõik</string>
<string name="menu_insert_marker">Lisa märgis</string>
<string name="menu_insert_photo">Foto lisamine</string>
<string name="menu_insert_gallery_img">Sisestage galerii pilt</string>
<string name="menu_markers">Märgised</string>
<string name="menu_insert_marker">Lisa marker</string>
<string name="menu_insert_photo">Lisa foto</string>
<string name="menu_insert_gallery_img">Sisesta pilt galeriist</string>
<string name="menu_markers">Markerid</string>
<string name="menu_search">Otsing</string>
<string name="menu_select_all">Vali kõik</string>
<string name="menu_settings">Seaded</string>
<string name="menu_settings">Seadistused</string>
<string name="menu_share">Jaga</string>
<string name="menu_show_on_map">Kuva kaardil</string>
<string name="menu_resume_track">Jätka rada</string>
@@ -222,33 +201,33 @@ limitations under the License.
<string name="sensor_state_heart_rate">Pulsisagedus</string>
<string name="sensor_state_heart_rate_value">%1$d lööki minutis</string>
<string name="sensor_state_power">Võimsus</string>
<string name="sensor_could_not_scan">Bluetooth seadmeid ei õnnestunud skannida (tõrge %1$i).</string>
<string name="bluetooth_disabled">Lubage Bluetooth.</string>
<string name="sensor_could_not_scan">Bluetooth seadmeid ei õnnestunud skannida (viga %1$i).</string>
<string name="bluetooth_disabled">Lülita Bluetooth sisse.</string>
<!-- Settings Advanced -->
<string name="settings_advanced">Täpsemad seaded</string>
<string name="settings_advanced">Täpsemad seadistused</string>
<!-- Settings Chart -->
<string name="settings_chart_by_distance">Vahemaa järgi</string>
<string name="settings_chart_by_time">Aja järgi</string>
<!-- Settings Recording -->
<string name="settings_recording_show_on_lockscreen_while_recording_title">Kuva lukustuskuval statistikat</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Salvestamise ajal näidake statistikat ilma seadet avamata.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Hoia ekraan sisse lülitatud</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Salvestamise ajal hoidke ekraani sisse lülitatud.</string>
<string name="settings_recording_show_on_lockscreen_while_recording_title">Kuva lukustusvaates statistikat</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Salvestamise ajal näita statistikat ilma seadet avamata.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Hoia ekraan sisselülitatuna</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Salvestamise ajal hoia ekraan sisselülitatuna.</string>
<string name="settings_recording_default_activity_title">Tegevuse vaiketüüp</string>
<string name="settings_recording_location_frequency_summary">%1$s salvestatud asukohtade vahel</string>
<string name="settings_recording_location_frequency_summary">%1$s salvestatud asukohtade kontrollimise vahel</string>
<string name="settings_recording_max_recording_distance_summary">Kui vahemaa kahe asukoha vahel on üle %1$s, luuakse uus lõik</string>
<string name="settings_recording_max_recording_distance_title">Max vahemaa</string>
<string name="settings_recording_max_recording_distance_title">Suurim vahemaa</string>
<string name="settings_recording_min_recording_distance_title">Salvestamiskauguse intervall</string>
<string name="settings_recording_min_recording_interval_title">Salvestamisaja intervall</string>
<string name="settings_recording_min_recording_interval_title">Salvestamisaja välp</string>
<string name="settings_recording_min_required_accuracy_title">GPS-i täpsus</string>
<string name="settings_recording_track_name_date_iso_8601_option">Kuupäev (ISO 8601)</string>
<string name="settings_recording_track_name_date_local_option">Kuupäev (kohalik)</string>
<string name="settings_recording_track_name_number_option">Number</string>
<string name="settings_recording_track_name_title">Raja vaikenimi</string>
<!-- Settings Reset -->
<string name="settings_reset_confirm_message">Taastatakse kõikide seadete vaikeväärtus.</string>
<string name="settings_reset_confirm_title">Kas lähtestada kohe?</string>
<string name="settings_reset_done">Kõik seaded on taastatud vaikeväärtustena</string>
<string name="settings_reset_confirm_message">Taastatakse kõikide seadistuste vaikeväärtus.</string>
<string name="settings_reset_confirm_title">Kas lähtestame kohe?</string>
<string name="settings_reset_done">Kõik seadistused on taastatud vaikeväärtustena</string>
<!-- Settings Sensor -->
<!-- Settings Stats -->
<string name="settings_stats_rate_title">Eelistatud määr</string>
@@ -256,10 +235,8 @@ limitations under the License.
<string name="settings_stats_units_metric">Meetermõõdustik (km, m)</string>
<string name="settings_stats_units_title">Eelistatud ühikud</string>
<!-- Share Track -->
<string name="share_track_share_file_body">Teile võib see rada huvi pakkuda.
\n
\n%1$s</string>
<string name="share_track_subject">Ma soovin teiega rada jagada</string>
<string name="share_track_share_file_body">Sulle võib see rada huvi pakkuda. \n \n%1$s</string>
<string name="share_track_subject">Ma soovin sinuga rada jagada</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Keskmine liikumistempo</string>
<string name="stats_average_moving_speed">Keskmine liikumiskiirus</string>
@@ -267,8 +244,8 @@ limitations under the License.
<string name="stats_average_speed">Keskmine kiirus</string>
<string name="stats_altitude">Kõrgus</string>
<string name="stats_fastest_pace">Kiireim tempo</string>
<string name="stats_gain">Saavutatud</string>
<string name="stats_max_speed">Max kiirus</string>
<string name="stats_gain">Kasv</string>
<string name="stats_max_speed">Maksimaalne kiirus</string>
<string name="stats_moving_time">Liikumisaeg</string>
<string name="stats_pace">Tempo</string>
<string name="stats_speed">Kiirus</string>
@@ -280,22 +257,22 @@ limitations under the License.
<string name="track_markers">Raja %1$s markerid</string>
<!-- Track Delete -->
<string name="track_delete_one_confirm_message">Rada ja selle markerid kustutatakse seadmest jäädavalt.</string>
<string name="track_delete_one_confirm_title">Kas kustutada rada?</string>
<string name="track_delete_one_confirm_title">Kas kustutame raja?</string>
<string name="track_delete_multiple_confirm_message">Valitud rajad ja nende markerid kustutatakse seadmest jäädavalt.</string>
<string name="track_delete_progress_message">Kustutamine…</string>
<string name="track_delete_progress_message">Kustutan…</string>
<!-- Track Detail -->
<string name="track_detail_stats_tab">Statistika</string>
<!-- Track Edit -->
<string name="track_edit_activity_type">Tegevus (nt matkamine)</string>
<string name="track_edit_activity_type_hint">Tegevuse tüüp</string>
<!-- Track List -->
<string name="track_list_empty_message">Alustage oma järgmise seikluse salvestamist siin</string>
<string name="track_list_empty_message">Alusta oma järgmise seikluse salvestamist siin</string>
<!-- Track Widget -->
<!-- Tracks -->
<!-- Unit -->
<string name="unit_feet">jalga</string>
<string name="unit_kilometer">km</string>
<string name="unit_kilometer_per_hour">km/h</string>
<string name="unit_kilometer_per_hour">km/t</string>
<string name="unit_meter">m</string>
<string name="unit_mile">miili</string>
<string name="unit_mile_per_hour">mph</string>
@@ -333,7 +310,7 @@ limitations under the License.
<string name="waypoint_type_fire_place">kamin</string>
<string name="waypoint_type_fire_station">tuletõrjedepoo</string>
<string name="waypoint_type_fishing">kalapüük</string>
<string name="waypoint_type_gas_station">bensiinijaam</string>
<string name="waypoint_type_gas_station">tankla</string>
<string name="waypoint_type_golf_course">golfiväljak</string>
<string name="waypoint_type_grocery_store">toidukauplus</string>
<string name="waypoint_type_hair_dresser">juuksur</string>
@@ -341,7 +318,7 @@ limitations under the License.
<string name="waypoint_type_hiking_path">matkarada</string>
<string name="waypoint_type_hotel">hotell</string>
<string name="waypoint_type_house">maja</string>
<string name="waypoint_type_information">teave</string>
<string name="waypoint_type_information">infotahvel</string>
<string name="waypoint_type_intersection">ristmik</string>
<string name="waypoint_type_mail_box">postkast</string>
<string name="waypoint_type_market">turg</string>
@@ -362,66 +339,66 @@ limitations under the License.
<string name="waypoint_type_toilet">tualett</string>
<string name="waypoint_type_tourist_information">turismiinfo</string>
<string name="waypoint_type_tree">puu</string>
<string name="waypoint_type_tunnel">tunnelit</string>
<string name="waypoint_type_tunnel">tunnel</string>
<string name="waypoint_type_waterfall">juga</string>
<string name="waypoint_type_water_fountain">purskkaev</string>
<string name="waypoint_type_waypoint">teekonnapunkt</string>
<!-- Share image -->
<string name="share_image_subject">Tahaksin teiega pilti jagada</string>
<string name="share_image_body">Ma arvan, et see pilt võiks teid huvitada.</string>
<string name="share_image_subject">Tahaksin sinuga pilti jagada</string>
<string name="share_image_body">Ma arvan, et see pilt võiks sind huvitada.</string>
<string name="open_track_as_trackfileformat">Ava kui %1$s</string>
<string name="activity_type_escooter">elektritõukeratas</string>
<string name="description_altitude_loss">Kõrguse langus: %1$d m (%2$d jalga)</string>
<string name="export_all_do_it_for_all">tehke seda iga konfilkti puhul</string>
<string name="export_error_post_workout">Treeningujärgne eksport nurjus, palun kontrolli ekspordikataloogi.</string>
<string name="import_thread_interrupted">Lõim katkestatud</string>
<string name="export_all_do_it_for_all">tee seda iga konfilkti puhul</string>
<string name="export_error_post_workout">Treeningujärgne eksport ei õnnestunud, palun kontrolli ekspordikausta.</string>
<string name="import_thread_interrupted">Lõim on katkestatud</string>
<string name="import_parser_error">Parseri viga: %1$s</string>
<string name="import_unable_to_import_file">Faili importimine nurjus: %1$s</string>
<string name="import_unable_to_import_file">Faili importimine ei õnnestunud: %1$s</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Lõpetatud ühe veaga</item>
<item quantity="other">Lõpetatud %1$d veaga</item>
</plurals>
<string name="gps_fixed_and_ready">GPS fikseeritud ja valmis</string>
<string name="import_prevent_reimport">Olete määranud vältida raja re-importimist</string>
<string name="gps_fixed_and_ready">GPS on fikseeritud ja valmis</string>
<string name="import_prevent_reimport">Oled seadistanud raja uuesti importimise vältimise</string>
<string name="generic_overwrite">Kirjuta üle</string>
<string name="export_progress_summary_overwrite_msg">Ülekirjutatud rajad</string>
<string name="export_track_errors">Eksportimata rajad</string>
<string name="export_track_already_exists_msg">Rada %1$s on sihtkataloogis juba olemas.</string>
<string name="aggregated_stats_empty_message">Salvestage oma esimene pala, et näha koondatud statistikat</string>
<string name="settings_prevent_reimport_tracks_title">Palade reimportimise vältimine</string>
<string name="export_track_already_exists_msg">Rada %1$s on sihtkaustas juba olemas.</string>
<string name="aggregated_stats_empty_message">Salvesta oma esimene rada ja näed koondstatistikat</string>
<string name="settings_prevent_reimport_tracks_title">Väldi radade kordusimportimist</string>
<string name="settings_night_mode_option_night">Öö</string>
<string name="settings_night_mode_option_day">Päev</string>
<string name="settings_night_mode_option_system">Süsteem</string>
<string name="settings_night_mode_title">UI teema</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Salvestamise ajal esitada täisekraanil.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Täisekraan</string>
<string name="settings_night_mode_option_system">Süsteemi kujundus</string>
<string name="settings_night_mode_title">Kasutajaliidese kujundus</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Salvestamise ajal esita täisekraanivaates.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Täisekraanivaade</string>
<string name="settings_default_trackfileformat">Failivormingu eksport/jagamine</string>
<string name="sensor_state_power_avg">Keskmine Võimsus</string>
<string name="sensor_state_heart_rate_max">Maksimaalne pulss</string>
<string name="sensor_state_heart_rate_avg">Keskmine pulsisagedus</string>
<string name="sensor_state_cadence_max">Max kadentsus</string>
<string name="sensor_state_cadence_avg">Keskmine kadentsus</string>
<string name="sensor_state_cadence_max">Maksimaalne väntamiskiirus</string>
<string name="sensor_state_cadence_avg">Keskmine väntamiskiirus</string>
<string name="settings_sensor_wheel_circumference">Velje ümbermõõt (mm)</string>
<string name="stats_loss">Kahjum</string>
<string name="stats_loss">Kadu</string>
<string name="track_detail_intervals_tab">Intervallid</string>
<string name="stats_split_interval">Jagatud intervall</string>
<string name="interval_list_empty_message">Ei ole ühtegi intervalli veel</string>
<string name="hold_to_stop">Hoidke lõpetada</string>
<string name="instant_export_enabled_summary">Ekspordi pala salvestusruumi pärast salvestamise lõpetamist</string>
<string name="settings_default_export_uri_title">Track ekspordi kataloog</string>
<string name="interval_list_empty_message">Veel ei ole ühtegi intervalli</string>
<string name="hold_to_stop">Lõpetamiseks hoia all</string>
<string name="instant_export_enabled_summary">Peale salvestamise lõpetamist ekspordi rada salvestusruumi</string>
<string name="settings_default_export_uri_title">Raja ekspordikaust</string>
<string name="instant_export_enabled_title">Vahetu treeningujärgne eksport</string>
<string name="export_progress_summary_errors_msg">Viga</string>
<string name="export_progress_summary_skip_msg">Vahele jäetud rajad</string>
<string name="export_progress_summary_new_msg">Uued rajad eksporditud</string>
<string name="export_progress_summary_skip_msg">Vahelejäetud rajad</string>
<string name="export_progress_summary_new_msg">Uued rajad on eksporditud</string>
<string name="generic_open_track">Avatud rada</string>
<string name="generic_completed">Lõpetatud</string>
<string name="generic_click_twice_cancel">Tühistamiseks vajutage uuesti tagasi</string>
<string name="gps_disabled_msg">GPS keelatud</string>
<string name="generic_click_twice_cancel">Tühistamiseks vajuta uuesti tagasi-nuppu</string>
<string name="gps_disabled_msg">GPS on lülitatud välja</string>
<string name="import_unsupported_format">Toetamata failivorming</string>
<string name="import_no_kml_file_found">kml faili ei leitud KMZ-st</string>
<string name="import_no_kml_file_found">KML-faili ei leidunud KMZ-s</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="import_error_list_dialog_title">Faile pole imporditud</string>
<string name="import_file_imported">Fail %1$s imporditud</string>
<string name="import_file_imported">%1$s fail on imporditud</string>
<string name="import_progress_summary_ok_msg">Imporditud failid</string>
<string name="import_progress_summary_exists_msg">Juba olemas</string>
<string name="generic_show_errors">Näita vigu</string>
@@ -433,10 +410,10 @@ limitations under the License.
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
<string name="menu_voice_rate">Hääle kiirus</string>
<string name="track_delete_not_recording">Seda pala ei saa kustutada, kuna see on praegu salvestatud.</string>
<string name="settings_sensor_cycling_cadence">Rütm</string>
<string name="track_delete_not_recording">Seda rada ei saa kustutada, kuna see on hetkel salvestamisel.</string>
<string name="settings_sensor_cycling_cadence">Väntamissagedus</string>
<string name="settings_sensor_cycling_distance_speed">Kaugus ja kiirus</string>
<string name="settings_sensor_running_speed_and_cadence">Kaugus, kiirus ja kadents</string>
<string name="settings_sensor_running_speed_and_cadence">Kaugus, kiirus ja väntamissagedus</string>
<string name="stats_clock">Kell</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (soovitatav)</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
@@ -445,61 +422,178 @@ limitations under the License.
<string name="settings_cycling_sensor">Jalgrattasõit</string>
<string name="settings_import_section">Import</string>
<string name="permission_bluetooth_failed">OpenTracks vajab Bluetooth-i kasutamiseks luba.</string>
<string name="permission_bluetooth_failed_rejected">Peate Opentracks\'ile süsteemiseadetes andma käsitsi loa lähedalasuvatele seadmetele.</string>
<string name="permission_bluetooth_failed_rejected">Pead Opentracks\'ile süsteemiseadistustes andma loa lähedalasuvate seadmete kasutamiseks (Nearby Devices).</string>
<string name="field_set_primary">Esmane</string>
<string name="generic_choose_an_option">Valige suvand</string>
<string name="settings_layout_reset_done">Kõik paigutusseaded on muudetud vaikeväärtusteks</string>
<string name="settings_all_reset">Lähtestage</string>
<string name="generic_choose_an_option">Vali eelistus</string>
<string name="settings_layout_reset_done">Kõik paigutuse seasistused on muudetud vaikeväärtusteks</string>
<string name="settings_all_reset">Lähtesta</string>
<string name="settings_defaults_title">Vaikeühikud ja -tegevused</string>
<string name="settings_defaults_summary">Eelistatud Ühikud, Vaiketegevused, Raja Vaikenimi</string>
<string name="settings_defaults_summary">Eelistatud Ühikud, vaiketegevused, raja vaikimisi nimi</string>
<string name="settings_ui_title">Kasutajaliides</string>
<string name="settings_ui_summary">Välimus, Salvestamise Paigutus, Teema</string>
<string name="settings_ui_summary">Välimus, salvestamise paigutus, kujundus</string>
<string name="settings_gps_summary">Ajavahemik, vahemaa intervall, maksimaalne kaugus, täpsus</string>
<string name="settings_sensors_title">Andurid</string>
<string name="settings_announcements_statistics_title">Häälteated</string>
<string name="settings_announcements_summary">Aeg, Kaugus, Hääle Kiirus</string>
<string name="settings_announcements_statistics_title">Statistilised teated</string>
<string name="settings_announcements_summary">Aeg, kaugus, hääle kiirus</string>
<string name="settings_units_category">Ühikud</string>
<string name="settings_import_export_summary">Import, Eksport, Automaatne Eksport</string>
<string name="settings_import_export_title">Import/Eksport</string>
<string name="settings_import_export_summary">Import, eksport, automaatne eksport</string>
<string name="settings_import_export_title">Import ja eksport</string>
<string name="settings_gps_sensor">GPS</string>
<string name="settings_reset_summary">Lähtestage Seaded vaikeväärtustele</string>
<string name="settings_recording_customize_layout_title">Kohandage oma salvestuspaigutust</string>
<string name="settings_reset_summary">Lähtesta seadistused vaikeväärtusteks</string>
<string name="settings_recording_customize_layout_title">Kohanda oma salvestuse paigutust</string>
<string name="settings_recording_customize_layout_select_columns">Andmeväljad rea kohta</string>
<string name="settings_export_section">Eksport</string>
<string name="settings_announcements_totaltime_frequency">Ajavahemik</string>
<string name="settings_layout_reset">Kohandatud Paigutuse Lähtestamine</string>
<string name="settings_layout_reset_confirm_title">Lähtestage oma paigutused\?</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (soovitatav)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="settings_layout_reset">Lähtesta kohandatud paigutus</string>
<string name="settings_layout_reset_confirm_title">Kas lähtestad oma paigutused?</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/t (soovitatav)</string>
<string name="value_float_mile_hour">%1$.1f mi/t</string>
<string name="stats_sensors_power">Võim</string>
<string name="stats_sensors_cadence">Rütm</string>
<string name="stats_sensors_heart_rate">Südamerütm</string>
<string name="field_set_secondary">Teisene</string>
<string name="settings_sensors_summary">Rattaandurid, Ratta Ümbermõõt, Jooksuandurid</string>
<string name="settings_activity_defaults_category">Tegevuse Vaikeväärtused</string>
<string name="field_remove_from_layout">Eemaldama</string>
<string name="stats_coordinates">Laiuskraad/Pikkuskraad</string>
<string name="settings_layout_reset_confirm_message">Kõik paigutuse seaded tagastatakse vaikeväärtustele. See ei kustuta seadmes olevaid lugusid.</string>
<string name="settings_running_sensor">Töötama</string>
<string name="settings_sensors_summary">Rattaandurid, ratta ümbermõõt, jooksuandurid</string>
<string name="settings_activity_defaults_category">Tegevuse vaikeväärtused</string>
<string name="field_remove_from_layout">Eemalda</string>
<string name="stats_coordinates">Laiuskraad/pikkuskraad</string>
<string name="settings_layout_reset_confirm_message">Kõik paigutuse seadistused muudetakse vaikeväärtusteks. See ei kustuta seadmes olevaid radasid.</string>
<string name="settings_running_sensor">Töötab</string>
<string name="settings_generic_sensor">Geneeriline</string>
<string name="custom_layout_list_layout_removed">Paigutus eemaldati</string>
<string name="custom_layout_list_layout_removed">Paigutus on eemaldatud</string>
<string name="menu_add_custom_layout">Uus paigutus</string>
<string name="generic_undo">Võta tagasi</string>
<string name="menu_select_layout">Valige paigutus</string>
<string name="menu_select_layout">Vali paigutus</string>
<string name="stats_custom_layout_default_layout">Vaikimisi paigutus</string>
<string name="custom_layout_select_layout">Valige paigutus</string>
<string name="custom_layout_select_layout">Vali paigutus</string>
<string name="custom_layout_list_edit_already_exists">Paigutuse nimi on juba olemas</string>
<string name="custom_layout_list_edit_name">Sisestage paigutuse nimi, et lisada</string>
<string name="custom_layout_list_title">Plaanid</string>
<string name="custom_layout_list_edit_name">Lisamiseks sisesta paigutuse nimi</string>
<string name="custom_layout_list_title">Paigutused</string>
<string name="generic_filter">Filtreeri</string>
<string name="generic_from">Alates</string>
<string name="generic_to">Kuni</string>
<string name="menu_filter">Filtreeri</string>
<string name="settings_sensor_bluetooth_service_filter_on">Näitab ainult neid Bluetooth-seadmeid, mis teatavad nõutavatest teenustest</string>
<string name="settings_sensor_bluetooth_service_filter_on">Näita vaid neid Bluetoothi-seadmeid, mis teatavad nõutavatest teenustest</string>
<string name="aggregated_stats_filter_no_results">Määratud filtri jaoks ei ole tegevusi</string>
<string name="generic_today">Täna</string>
<string name="export_dir_not_writable">" (pole kirjutatav!)"</string>
<string name="export_cannot_write_to_dir">Ei saa kirjutada ekspordikataloogi.</string>
<string name="export_cannot_write_to_dir">Ei saa kirjutada ekspordikausta.</string>
<string name="generic_yesterday">Eile</string>
<string name="no_compatible_file_manager_installed">Ühilduv failihaldur ei ole paigaldatud</string>
</resources>
<string name="no_compatible_file_manager_installed">Ühtegi ühilduvat failihaldurit ei ole paigaldatud</string>
<string name="settings_announcements_average_heart_rate">Keskmine pulss</string>
<string name="settings_announcements_lap_heart_rate">Pulss ringi ajal</string>
<string name="settings_announcements_average_speed_pace">Keskmine kiirus/samm</string>
<string name="help_data_google_play_services_content">Ei</string>
<string name="activity_type_kickscooter">tõukeratas</string>
<string name="activity_type_swimming">ujumine</string>
<string name="activity_type_swimming_open">ujumine välitingimustes</string>
<string name="activity_type_workout">treening</string>
<string name="description_speed_nautical">Kiirus (sõlmedes)</string>
<string name="generic_dismiss">Loobu</string>
<string name="gps_recording_status">%1$s: %2$s</string>
<string name="image_discard">Loobu</string>
<string name="gps_recording_without_signal">OpenTracks salvestab ilma GPS-andmeteta.</string>
<string name="create_marker_error">Markeri lisamine ei õnnestu. Palun proovi uuesti.</string>
<string name="marker_detail_title">%1$d / %2$d: %3$s</string>
<string name="menu_export_all_one_file">Ekspordi kõik (üks fail kõikide radade kohta)</string>
<string name="sensor_state_pressure">Baromeeter</string>
<string name="settings_announcements_idle_title">Jõude</string>
<string name="settings_export_filename_title">Failinime vorming</string>
<string name="settings_chart_display_elevation_title">Näita kõrgusi</string>
<string name="settings_charts_title">Diagrammid</string>
<string name="settings_chart_display_pace_or_speed_title">Näita tempot või kiirust</string>
<string name="description_pace_nautical">Tempo (min/merem)</string>
<string name="settings_announcements_segment_average_speed_pace">Keskmine kiirus/tempo (segment)</string>
<string name="settings_announcements_lap_speed_pace">Ringi kiirus/tempo</string>
<string name="stats_average_segment_pace">Keskmine segmendi tempo</string>
<string name="pace">Tempo</string>
<string name="settings_locale_title">Keel</string>
<string name="settings_recording_idle_timeout_title">Jõudeeoleku lävi</string>
<string name="settings_announcements_unit">Ühik vahemaa/kiiruse jaoks</string>
<string name="lap_time">Ringiaeg</string>
<string name="voice_per_nautical_mile">meremiili kohta</string>
<string name="voice_per_mile">miili kohta</string>
<string name="voice_per_kilometer">kilomeetri kohta</string>
<string name="voice_on_device_speaker_title">Kasuta nutiseadme kõlarit</string>
<string name="lap_speed">Ringi kiirus</string>
<string name="speed">Keskmine liikumiskiirus</string>
<string name="total_distance">Vahemaa kokku</string>
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {1 kilomeeter} other {{n,number,#.0} kilometeerit} }</string>
<string name="voiceDistanceMilesPlural">{n, plural, =1 {1 miil} other {{n,number,#.0} miili} }</string>
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 meremiil} other {{n,number,#.0} meremiili} }</string>
<string name="average_heart_rate">Keskmine pulss</string>
<string name="current_heart_rate">Pulss hetkel</string>
<string name="lap_heart_rate">Pulss ringi ajal</string>
<string name="no_compatible_camera_installed">Ühtegi ühilduvat kaamerarakendust pole paigaldatud</string>
<string name="no_compatible_gallery_installed">Ühtegi ühilduvat pildigaleriirarakendust pole paigaldatud</string>
<string name="error_crash_title">%1$s jooksis kokku</string>
<string name="share_crash_report">Jaga kokkujooksmise aruannet</string>
<string name="report_bug">Teata veast</string>
<string name="sensor_state_power_max">Maksimaalne võimsus</string>
<string name="settings_announcements_idle_category_title">Jõudeoleku teated</string>
<string name="settings_chart_display_heart_rate_title">Näita pulssi</string>
<string name="settings_recording_title">Salvestan</string>
<string name="value_integer_knots">%1$d sõlme</string>
<string name="value_float_knots">%1$.1f sõlme</string>
<string name="value_float_knots_recommended">%1$.1f sõlme (soovitatud)</string>
<string name="value_internal_sensor">Sisemine andur</string>
<string name="value_int_seconds">%1$d s (soovitatud)</string>
<string name="voiceIdle">Kohe olen jõude.</string>
<string name="voiceHoursPlural">{n, plural, =1 {1 tund} other {{n,number} tundi} }</string>
<string name="voiceMinutesPlural">{n, plural, =1 {1 minut} other {{n,number} minutit} }</string>
<string name="voiceSecondsPlural">{n, plural, =1 {1 sekund} other {{n,number} sekundit} }</string>
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {1 kilomeeter tunnis} other {{n,number,#.0} kilomeetrit tunnis} }</string>
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 miil tunnis} other {{n,number,#.0} miili tunnis} }</string>
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 sõlm} other {{n,number,#.0} sõlme} }</string>
<string name="always">Alati</string>
<string name="power">Võimsus</string>
<string name="power_x_watt">Võimsus {x, plural, =1 {1 vatt} other {# vatti} }</string>
<string name="settings_announcements_lap_power">Ringi võimsus</string>
<string name="settings_recording_location_distance_summary">%1$s salvestatud asukohtade vahel</string>
<string name="settings_night_mode_option_night_dynamic">Öö (dünaamiliste värvidega)</string>
<string name="settings_night_mode_option_night_oled">OLED-ekraani sõbralik</string>
<string name="settings_night_mode_option_night_oled_dynamic">OLED-ekraani sõbralik (dünaamiliste värvidega)</string>
<string name="settings_recording_show_gps_disabled_warning_title">GPS-i väljalülitamise hoiatus</string>
<string name="settings_recording_show_gps_disabled_warning_summary">Salvestamise ajal näita hoiatust GPS-i väljalülitamise kohta.</string>
<string name="settings_recording_min_required_accuracy_summary">Kui täpsus on üle %1$s, siis asukohta eiratakse</string>
<string name="settings_locale_system_default">Süsteemi vaikeväärtus</string>
<string name="settings_night_mode_option_system_dynamic">Süsteemi kujundus (dünaamiliste värvidega)</string>
<string name="settings_night_mode_option_day_dynamic">Päev(dünaamiliste värvidega)</string>
<string name="settings_sensor_bluetooth_service_filter_title">Filtreeri Bluetoothi seadmeid</string>
<string name="settings_sensor_bluetooth_service_filter_off">Näita kõiki tuvastatavaid Bluetoothi seadmeid</string>
<string name="settings_sensor_heart_rate_max">Tsoonid: maksimaalne pulss (bpm)</string>
<string name="settings_stats_units_imperial_meter">Inglise mõõtühikud (miili, meetrit)</string>
<string name="settings_api_title">Avalik API</string>
<string name="settings_api_summary">API muude rakenduste jaoks kaugjuhtimiseks ja statistikaks</string>
<string name="permission_recording_failed">OpenTracks vajab luba sinu asukoha ja lähikonnas asuvate seadmete tuvastamiseks.</string>
<string name="sensor_state_cadence_unit">rpm</string>
<string name="sensor_state_heart_rate_unit">lm</string>
<string name="sensor_state_power_unit">W</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
<string name="settings_theme_switch_restart">Kujunduse muutmine võib vajada rakenduse uuesti käivitamist.</string>
<string name="settings_ui_dynamic_colors_title">Dünaamilised värvid</string>
<string name="settings_ui_dynamic_colors_summary">Pruugi kasutaja määratud dünaamilisi värve (vaid Android 12-s ja uuemates; eeldab rakenduse käivitamist)</string>
<string name="settings_stats_units_nautical">Merenduse mõõtühikud (meremiili, jalga)</string>
<string name="settings_public_api_package_title">Paketi nimi avaliku API jaoks</string>
<string name="settings_public_api_enabled_title">Avalik API</string>
<string name="settings_public_api_enabled_summary_on">Muud paigaldatud rakendused võivad alustada salvestamist ja lõpetada seda.</string>
<string name="settings_public_api_enabled_summary_off">Vaid OpenTracks võib alustada salvestamist ja lõpetada seda.</string>
<string name="settings_public_api_disabled_toast">OpenTracksi avalik API on lülitatud välja: seadistustest saad ta lülitada sisse.</string>
<string name="settings_public_api_dashboard_enabled_title">Andmete automaatne teisaldamine</string>
<string name="settings_public_api_dashboard_enabled_summary_on">Rakendus, mis alustas salvestamist, saab ka ligi salvestatud andmetele.</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Salvestatud andemeid ei jagata teiste rakendustega automaatselt.</string>
<string name="stats_elapsed_segment_time">Segmendile kulunud aeg</string>
<string name="stats_gain_loss">Juurdekasv ja vähenemine</string>
<string name="track_delete_number_of_tracks">%1$d rada</string>
<string name="track_discarding">Loobun rajast</string>
<string name="track_edit_time_offset">Ajavööndi nihe</string>
<string name="unit_nautical_mile">meremiili</string>
<string name="unit_knots">sõlme</string>
<string name="unit_minute_per_nautical_mile">min/merem</string>
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
<string name="show_on_dashboard">OpenTracksi juhtpaneeli API</string>
<string name="show_on_dashboard_not_installed">Pole paigaldatud ühtegi rakendust, mis toetaks OpenTracksi juhtpaneeli API-t.</string>
<string name="introduction_osm_dashboard">Kaardi kuvamine pole OpenTracks-i automaatselt lisatud. Oma salvestiste kaardil vaatamiseks palun paigalda OSMDashboard.</string>
<string name="settings_recording_min_sampling_interval_title">Proovivõtu ajavahemik</string>
<string name="voice_announce_lap_power_key">RingiVõimsuseHäälteade</string>
</resources>
+1
View File
@@ -621,4 +621,5 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="voiceIdle">Passaggio a inattivo.</string>
<string name="settings_announcements_segment_average_speed_pace">Velocità/Ritmo medi (segmento)</string>
<string name="stats_average_segment_pace">Ritmo medio nel segmento</string>
<string name="stats_elapsed_segment_time">Tempo trascorso nel segmento</string>
</resources>
+2 -1
View File
@@ -41,7 +41,8 @@ limitations under the License.
<string name="sensor_unit_beats_per_minute" translatable="false">bpm</string>
<string name="sensor_unit_rounds_per_minute" translatable="false">rpm</string>
<string name="sensor_unit_power" translatable="false">w</string>
<string name="sensor_unit_power" translatable="false">W</string>
<string name="sensor_unit_temperature" translatable="false">C</string>
<string name="value_unknown" translatable="false">-</string>
<string name="empty" translatable="false" />
+2 -93
View File
@@ -20,6 +20,7 @@
<string name="settings_reset_key" translatable="false">settingsReset</string>
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
<string name="settings_sensor_bluetooth_temperature_key" translatable="false">bluetoothTemperature</string>
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
<string name="settings_sensor_bluetooth_cycling_power_key" translatable="false">bluetoothCyclingPowerSensor</string>
@@ -335,9 +336,6 @@
<!-- Stats layout customization -->
<string name="stats_custom_layout_key" translatable="false">statsCustomLayoutKey</string>
<!-- Stats layout customization: number of columns per row -->
<integer name="stats_custom_layout_columns_default" translatable="false">2</integer>
<!-- Stats layout customization: key -->
<string name="stats_custom_layouts_key" translatable="false">statsCustomLayoutFieldsKey</string>
<string name="stats_custom_layout_selected_layout_key" translatable="false">statsCustomSelectedLayoutKey</string>
@@ -361,100 +359,11 @@
<string name="stats_custom_layout_coordinates_key" translatable="false">coordinates</string>
<string name="stats_custom_layout_heart_rate_key" translatable="false">heart_rate</string>
<string name="stats_custom_layout_average_heart_rate_key" translatable="false">average_heart_rate</string>
<string name="stats_custom_layout_temperature_key" translatable="false">temperature</string>
<string name="stats_custom_layout_cadence_key" translatable="false">cadence</string>
<string name="stats_custom_layout_average_cadence_key" translatable="false">average_cadence</string>
<string name="stats_custom_layout_power_key" translatable="false">power</string>
<string name="stats_custom_layout_clock_key" translatable="false">time</string>
<!--
Default dataFields for stats layout customization in a set of arrays.
Every array has two items:
- Key value for the dataField.
- Comma separated flags for: visible, primary and span/wide for the dataField.
-->
<string-array name="stats_custom_layout_fields_default_value_0">
<item>@string/stats_custom_layout_distance_key</item>
<item>1,1,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_1">
<item>@string/stats_custom_layout_total_time_key</item>
<item>1,1,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_2">
<item>@string/stats_custom_layout_speed_key</item>
<item>1,1,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_3">
<item>@string/stats_custom_layout_moving_time_key</item>
<item>1,1,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_4">
<item>@string/stats_custom_layout_average_speed_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_5">
<item>@string/stats_custom_layout_max_speed_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_6">
<item>@string/stats_custom_layout_average_moving_speed_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_7">
<item>@string/stats_custom_layout_altitude_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_8">
<item>@string/stats_custom_layout_gain_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_9">
<item>@string/stats_custom_layout_loss_key</item>
<item>1,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_10">
<item>@string/stats_custom_layout_pace_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_11">
<item>@string/stats_custom_layout_average_moving_pace_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_12">
<item>@string/stats_custom_layout_average_pace_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_13">
<item>@string/stats_custom_layout_average_segment_pace_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_14">
<item>@string/stats_custom_layout_elapsed_segment_time_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_15">
<item>@string/stats_custom_layout_fastest_pace_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_16">
<item>@string/stats_custom_layout_coordinates_key</item>
<item>0,0,1</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_17">
<item>@string/stats_custom_layout_heart_rate_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_18">
<item>@string/stats_custom_layout_cadence_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_19">
<item>@string/stats_custom_layout_power_key</item>
<item>0,0,0</item>
</string-array>
<string-array name="stats_custom_layout_fields_default_value_20">
<item>@string/stats_custom_layout_clock_key</item>
<item>0,0,0</item>
</string-array>
<!-- Stats empty values -->
<string name="stats_empty_value_integer" translatable="false">0</string>
+3
View File
@@ -173,6 +173,7 @@ limitations under the License.
<string name="description_speed_ms">Speed (m/s)</string>
<string name="description_sensor_cadence">Cadence (rpm)</string>
<string name="description_sensor_heart_rate">Heart rate (bpm)</string>
<string name="description_sensor_temperature">Temperature (Celsius)</string>
<string name="description_sensor_power">Power (W)</string>
<string name="description_default_speed_or_pace">By activity type</string>
<string name="description_speed_imperial">Speed (mi/hr)</string>
@@ -312,6 +313,7 @@ limitations under the License.
<string name="sensor_state_heart_rate_unit">bpm</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
<string name="sensor_state_power">Power</string>
<string name="sensor_state_temperature">Thermometer</string>
<string name="sensor_state_pressure">Barometer</string>
<string name="sensor_state_power_avg">Avg Power</string>
<string name="sensor_state_power_max">Max Power</string>
@@ -471,6 +473,7 @@ limitations under the License.
<string name="stats_total_time">Total time</string>
<string name="stats_split_interval">Split interval</string>
<string name="stats_sensors_heart_rate">Heart rate</string>
<string name="stats_sensors_temperature">Temperature</string>
<string name="stats_sensors_cadence">Cadence</string>
<string name="stats_sensors_power">Power</string>
<string name="stats_clock">Clock</string>

Some files were not shown because too many files have changed in this diff Show More