Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 823dc1c12b | |||
| 26490f5a66 | |||
| d299dbdf06 | |||
| b5555e302f | |||
| 3030b4dfd9 | |||
| a5431b1cf5 | |||
| 9314221647 | |||
| 9bb9d5c283 | |||
| 1095a1442f | |||
| d373fdaaeb | |||
| 5278ba4d35 | |||
| 9b71b7f88c | |||
| bc96c2c05b | |||
| e655762c72 | |||
| 71c385a1a9 | |||
| b947a727e7 | |||
| 470f23afa8 | |||
| 0a1f9dfe41 | |||
| 6fcd19bf2e | |||
| 32d9705a4c | |||
| 2ac66c08f2 | |||
| 9234da4957 | |||
| 24e18347a6 | |||
| 3b776407fa | |||
| 399f90a8f0 | |||
| 4c68592a0c | |||
| b2c2047ca2 | |||
| 838671e2ce | |||
| 47a340578f | |||
| e9e48e4a79 | |||
| 40188774b1 | |||
| 2f0449c420 | |||
| 21376cd3da | |||
| e28c385e52 | |||
| 8d4418a584 | |||
| 85c7314fa8 | |||
| 06a05c82bc | |||
| 4d08729284 | |||
| 28bbf14efd | |||
| 5686322aa6 | |||
| a6c06efb62 | |||
| 80a5149207 | |||
| 5b31213770 | |||
| 19cbbb90f9 | |||
| 39e4a697c0 | |||
| 196de0c3d4 | |||
| 8aa02d009d | |||
| 9a89295b1f | |||
| 4c3c93511f | |||
| bfd6517a61 | |||
| 6334dcea04 | |||
| 5b2d9a5773 | |||
| f1ee0ea035 | |||
| 57afedb3a8 | |||
| 4e33ad4007 | |||
| eada954299 | |||
| 288fe27a57 | |||
| 54e8fd3ee3 | |||
| 68c8839ebb | |||
| 12e764c44a | |||
| 50eff0d7e8 | |||
| ee96226faf | |||
| f5ec91f0eb | |||
| b0575c0f5b | |||
| ccfcd3b306 | |||
| 7929d219ac | |||
| d6f4d3f599 | |||
| 4171d9cc4c | |||
| 9366c031b3 | |||
| 3454fe0cfa | |||
| 3ad2d725be | |||
| d3400dbdfb | |||
| dc4d2ecd89 | |||
| a1feb7d0d5 | |||
| 5cad99b237 | |||
| fb8a4de79b | |||
| b1b0a45f53 | |||
| e580a03115 |
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.16.0
|
||||
uses: reactivecircus/android-emulator-runner@v2.18.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
@@ -23,8 +23,6 @@ However, often only one value is provided.
|
||||
* Has updatable firmware that requires an account for garmin.com
|
||||
* This sensor reports speed data as cadence.
|
||||
A workaround is in place.
|
||||
* This sensor sometimes counts backwards (unknown software version).
|
||||
A workaround is in place.
|
||||
|
||||
* Wahoo Speed (Model: WFRPMSPD)
|
||||
|
||||
@@ -50,4 +48,5 @@ Also the distance is not computed.
|
||||
* QUARQ Red DZero Powermeter
|
||||
* Tacx Satori Smart
|
||||
* Wahoo Kickr v4.0
|
||||
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4098
|
||||
versionName "v3.18.2"
|
||||
versionCode 4176
|
||||
versionName "v3.18.5"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -90,7 +90,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
@@ -103,5 +102,6 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
v3.18.3: OpenTracks
|
||||
|
||||
Changes:
|
||||
- KML parsing does not require order of when/gx:coords
|
||||
- Track importer: reorders locations by timestamp
|
||||
- Removed rendered launcher icons for Android < 8.0.0 (API 26)
|
||||
(might look slightly different, but avoid problems for reproducible builds)
|
||||
|
||||
Bugfix:
|
||||
- Avoid crash in charts due to race condition
|
||||
|
||||
Developer:
|
||||
- Split GPX and KML importer (removed common inheritance)
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.18.4: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Speed/distance are consistently formatted
|
||||
- Intervals tab is now faster
|
||||
|
||||
Bugfix:
|
||||
- Cycling speed/distance sensor:
|
||||
- fixed parsing (UINT32)
|
||||
- transmit data to UI properly
|
||||
@@ -0,0 +1,12 @@
|
||||
v3.18.5: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Voice announcement speed configurable
|
||||
- API <26: Launcher icon omits background
|
||||
|
||||
Bugfixes:
|
||||
- Recording UI: show correct values
|
||||
- Recording UI: show (again) altitude in EGM2008
|
||||
|
||||
Developer:
|
||||
- Android12 permissions (BLUETOOTH_CONNECT)
|
||||
@@ -4,6 +4,8 @@ Fonctionnalités :
|
||||
|
||||
* Suivi d'activité : enregistre vos activités sportives et de plein air
|
||||
* Photos et repères : ajout de photos et de repères pendant l'enregistrement de l'activité
|
||||
* Rythme cardiaque : support des capteurs Bluetooth LE
|
||||
* Export : export des traces au format KML ou GPX
|
||||
* Dénivelé positif et négatif: via capteur de pression barométrique
|
||||
* Synthèse vocale
|
||||
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
|
||||
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
|
||||
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
|
||||
|
||||
@@ -1 +1 @@
|
||||
OpenTracks - Suivi d'activités sportives respectueux de votre vie privée.
|
||||
OpenTracks - Suivi d'activités sportives respectueux de votre vie privée.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
@@ -9,16 +10,18 @@ import androidx.test.espresso.DataInteraction;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
@@ -44,12 +47,15 @@ import static org.hamcrest.Matchers.is;
|
||||
public class EspressoCategoryUnitTest {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.ACCESS_FINE_LOCATION");
|
||||
public TestRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
private final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
@Rule
|
||||
public TestRule mLocaleRule = new PreferenceMetricUnitRule(context, true);
|
||||
|
||||
@Test
|
||||
public void changeBetweenCategoryUpdateProperlyUnitTest() {
|
||||
|
||||
@@ -4,7 +4,6 @@ package de.dennisguse.opentracks;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -19,10 +18,10 @@ import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.action.MotionEvents;
|
||||
import androidx.test.espresso.assertion.ViewAssertions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
@@ -56,7 +55,7 @@ public class EspressoDeleteTrackTest {
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
private MyIdlingResource idlingResource;
|
||||
|
||||
@@ -242,7 +241,7 @@ public class EspressoDeleteTrackTest {
|
||||
|
||||
private class MyIdlingResource implements IdlingResource {
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private ResourceCallback resourceCallback;
|
||||
|
||||
public MyIdlingResource(Context context) {
|
||||
|
||||
@@ -35,7 +35,7 @@ public class EspressoUITest {
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
public class PreferenceMetricUnitRule implements TestRule {
|
||||
private final Context context;
|
||||
private final boolean metricUnits;
|
||||
|
||||
public PreferenceMetricUnitRule(@NonNull Context context, boolean metricUnits) {
|
||||
this.context = context;
|
||||
this.metricUnits = metricUnits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, final Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
|
||||
|
||||
try {
|
||||
PreferencesUtils.setMetricUnits(sharedPreferences, context, metricUnits);
|
||||
base.evaluate();
|
||||
} finally {
|
||||
PreferencesUtils.setMetricUnits(sharedPreferences, context, previousMetricUnits);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import java.util.List;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class TestDataUtil {
|
||||
@@ -166,4 +167,20 @@ public class TestDataUtil {
|
||||
}
|
||||
return trackPoints;
|
||||
}
|
||||
|
||||
public static Pair<Track.Id, TrackStatistics> buildTrackWithTrackPoints(ContentProviderUtils contentProviderUtils, int numberOfPoints) {
|
||||
Track dummyTrack = new Track();
|
||||
dummyTrack.setId(new Track.Id(System.currentTimeMillis()));
|
||||
dummyTrack.setName("Dummy Track");
|
||||
contentProviderUtils.insertTrack(dummyTrack);
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
for (int i = 0; i < numberOfPoints; i++) {
|
||||
TrackPoint tp = TestDataUtil.createTrackPoint(i);
|
||||
contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId());
|
||||
trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
|
||||
}
|
||||
dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
||||
contentProviderUtils.updateTrack(dummyTrack);
|
||||
return new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_speed_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
|
||||
/**
|
||||
* Tests {@link ChartFragment}.
|
||||
*
|
||||
* @author Youtao Liu
|
||||
*/
|
||||
//TODO Add tests that check ChartFragment
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ChartFragmentTest {
|
||||
|
||||
private static final double HOURS_PER_UNIT = 60.0;
|
||||
|
||||
private ChartFragment chartFragment;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
boolean chartByDistance = false;
|
||||
chartFragment = ChartFragment.newInstance(chartByDistance);
|
||||
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nothing() {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
@@ -90,10 +90,14 @@ public class ExportImportTest {
|
||||
private Track.Id trackId;
|
||||
private Track.Id importTrackId;
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
@Before
|
||||
public void fileSetup() throws IOException {
|
||||
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
|
||||
tmpFileUri = Uri.fromFile(tmpFile);
|
||||
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -169,8 +173,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -209,8 +213,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -248,8 +252,8 @@ public class ExportImportTest {
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
KmzTrackImporter trackImporter = new KmzTrackImporter();
|
||||
importTrackId = trackImporter.importFile(context, tmpFileUri).get(0);
|
||||
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
|
||||
importTrackId = importer.importFile(tmpFileUri).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -288,8 +292,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
@@ -313,8 +317,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -362,8 +366,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -406,18 +410,18 @@ public class ExportImportTest {
|
||||
// TODO Not exported for GPX/KML
|
||||
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
||||
|
||||
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
if (trackPoint.hasLocation()) {
|
||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
if (trackPoint.hasSpeed()) {
|
||||
assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||
assertEquals("" + i, trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||
if (trackPoint.hasAltitude()) {
|
||||
assertEquals(trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
|
||||
}
|
||||
|
||||
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
|
||||
@@ -426,13 +430,13 @@ public class ExportImportTest {
|
||||
}
|
||||
|
||||
if (verifyHeartrate) {
|
||||
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
}
|
||||
if (verifyCadence) {
|
||||
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
}
|
||||
if (verifyPower) {
|
||||
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
}
|
||||
if (verifyAltitudeGain) {
|
||||
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
@@ -15,6 +16,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -29,13 +31,20 @@ import static org.junit.Assert.assertNotNull;
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyImportTest {
|
||||
public class GPXImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
@@ -43,63 +52,6 @@ public class LegacyImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data with statistics markers is imported and those ignored.
|
||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_with_statistics_marker() throws IOException {
|
||||
// given
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
/**
|
||||
* At least one valid location is required.
|
||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_without_locations() throws IOException {
|
||||
// given
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
|
||||
|
||||
// when
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
|
||||
*/
|
||||
@@ -107,12 +59,12 @@ public class LegacyImportTest {
|
||||
@Test
|
||||
public void gpx_with_pause_resume() throws IOException {
|
||||
// given
|
||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context));
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -138,7 +90,7 @@ public class LegacyImportTest {
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
|
||||
}
|
||||
|
||||
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||
static void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||
assertEquals(type, trackPoint.getType());
|
||||
|
||||
@@ -154,6 +106,5 @@ public class LegacyImportTest {
|
||||
} else {
|
||||
assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class KMLImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KML does not specify the order of gx:coords and when; only count has to be the same.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_order_location_and_when() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("", importedTrack.getCategory());
|
||||
assertEquals("", importedTrack.getDescription());
|
||||
assertEquals("", importedTrack.getName());
|
||||
assertEquals("", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(5, importedTrackPoints.size());
|
||||
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:21.767Z", null, null, null);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.042Z", 14.0, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.192Z", 14.001, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.318Z", 14.002, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-05-29T18:06:22.512Z", null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates / when might not be ordered by increasing time.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_time_decreases() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("", importedTrack.getCategory());
|
||||
assertEquals("", importedTrack.getDescription());
|
||||
assertEquals("", importedTrack.getName());
|
||||
assertEquals("", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(2, importedTrackPoints.size());
|
||||
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:21.767Z", 14.0, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:22.042Z", null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data with statistics markers is imported and those ignored.
|
||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_with_statistics_marker() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first 3 trackpoints
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
/**
|
||||
* At least one valid location is required.
|
||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_without_locations() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_when_locations_different() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
}
|
||||
}
|
||||
@@ -193,4 +193,31 @@ public class TrackStatisticsUpdaterTest {
|
||||
@Test
|
||||
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copy_constructor() {
|
||||
// given
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
|
||||
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
|
||||
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp2, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp4, GPS_DISTANCE);
|
||||
|
||||
// when
|
||||
TrackStatisticsUpdater copy = new TrackStatisticsUpdater(subject);
|
||||
subject.addTrackPoint(tp5, GPS_DISTANCE);
|
||||
copy.addTrackPoint(tp5, GPS_DISTANCE);
|
||||
|
||||
|
||||
// then
|
||||
assertEquals(55.287, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
assertEquals(55.287, copy.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
|
||||
@@ -25,6 +27,12 @@ import static org.junit.Assert.assertEquals;
|
||||
public class AnnouncementUtilsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_speed() {
|
||||
@@ -44,22 +52,31 @@ public class AnnouncementUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_metric_speed() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour Lap speed of 51.2 kilometers per hour", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first +
|
||||
" kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), true, true).first +
|
||||
" kilometers per hour Lap speed of " +
|
||||
StringUtils.getSpeedParts(context, lastInterval.getSpeed(), true, true).first +
|
||||
" kilometers per hour",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,22 +97,31 @@ public class AnnouncementUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_metric_pace() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer Lap time of 1 minute 10 seconds per kilometer", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first +
|
||||
" kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(true), true) +
|
||||
" per kilometer Lap time of " +
|
||||
buildAndGetTimeText(lastInterval.getSpeed().toPace(true), true) +
|
||||
" per kilometer",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,22 +142,31 @@ public class AnnouncementUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_imperial_speed() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour Lap speed of 31.8 miles per hour", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first +
|
||||
" miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), false, true).first +
|
||||
" miles per hour Lap speed of " +
|
||||
StringUtils.getSpeedParts(context, lastInterval.getSpeed(), false, true).first +
|
||||
" miles per hour",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -152,21 +187,60 @@ public class AnnouncementUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_imperial_pace() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement);
|
||||
//assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first +
|
||||
" miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(false), true) +
|
||||
" per mile Lap time of " +
|
||||
buildAndGetTimeText(lastInterval.getSpeed().toPace(false), true) +
|
||||
" per mile",
|
||||
announcement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an returns the text representing the duration's time.
|
||||
*
|
||||
* @param duration Duration object.
|
||||
* @param showFromMinutes show minutes tough it's 0.
|
||||
* @return text representing the duratin's time.
|
||||
*/
|
||||
private String buildAndGetTimeText(Duration duration, boolean showFromMinutes) {
|
||||
long hours = Math.abs(duration.getSeconds()) / 3600;
|
||||
long minutes = (Math.abs(duration.getSeconds()) % 3600) / 60;
|
||||
long seconds = Math.abs(duration.getSeconds()) % 60;
|
||||
String hUnit = hours > 1 || hours == 0 ? "hours" : "hour";
|
||||
String mUnit = minutes > 1 || minutes == 0 ? "minutes" : "minute";
|
||||
String sUnit = seconds > 1 || seconds == 0 ? "seconds" : "second";
|
||||
|
||||
String res = hours > 0 ? hours + " " + hUnit : "";
|
||||
if (hours > 0) {
|
||||
res += minutes > 0 || showFromMinutes ? " " + minutes + " " + mUnit : "";
|
||||
} else {
|
||||
res += minutes > 0 || showFromMinutes ? minutes + " " + mUnit : "";
|
||||
}
|
||||
if (hours > 0 || minutes > 0 || showFromMinutes) {
|
||||
res += " " + seconds + " " + sUnit;
|
||||
} else {
|
||||
res += seconds + " " + sUnit;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -153,15 +153,15 @@ public class StringUtilsTest {
|
||||
assertEquals("10", StringUtils.formatDecimal(10.1, 0));
|
||||
assertEquals("-0", StringUtils.formatDecimal(-0.1, 0));
|
||||
|
||||
assertEquals("0", StringUtils.formatDecimal(0.0, 2));
|
||||
assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
|
||||
assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
|
||||
assertEquals("10", StringUtils.formatDecimal(10, 2));
|
||||
assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
|
||||
assertEquals("0.00", StringUtils.formatDecimal(0.0, 2));
|
||||
assertEquals("0.10", StringUtils.formatDecimal(0.1, 2));
|
||||
assertEquals("1.10", StringUtils.formatDecimal(1.1, 2));
|
||||
assertEquals("10.00", StringUtils.formatDecimal(10, 2));
|
||||
assertEquals("10.10", StringUtils.formatDecimal(10.1, 2));
|
||||
assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
|
||||
assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
|
||||
assertEquals("-0.10", StringUtils.formatDecimal(-0.1, 2));
|
||||
|
||||
assertEquals("1", StringUtils.formatDecimal(0.99, 1));
|
||||
assertEquals("1.0", StringUtils.formatDecimal(0.99, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -181,7 +181,7 @@ public class StringUtilsTest {
|
||||
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
|
||||
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
|
||||
|
||||
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
|
||||
assertEquals("7.47 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
|
||||
assertEquals("12.0 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
|
||||
assertEquals("7.5 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class UintUtilsTest {
|
||||
|
||||
@Test
|
||||
public void diff() {
|
||||
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT16_MAX));
|
||||
assertEquals(65534, UintUtils.diff(1, 2, UintUtils.UINT16_MAX));
|
||||
|
||||
assertEquals(65530, UintUtils.diff(UintUtils.UINT16_MAX, 5, UintUtils.UINT16_MAX));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realData() {
|
||||
assertEquals(1, UintUtils.diff(381616, 381615, UintUtils.UINT32_MAX));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
@@ -10,6 +16,8 @@ import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -21,8 +29,12 @@ public class IntervalStatisticsTest {
|
||||
|
||||
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
|
||||
|
||||
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
|
||||
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
|
||||
@@ -41,12 +53,10 @@ public class IntervalStatisticsTest {
|
||||
// With 50 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(50);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
whenAndThen(50, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,12 +67,10 @@ public class IntervalStatisticsTest {
|
||||
// With 200 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
whenAndThen(200, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,12 +81,10 @@ public class IntervalStatisticsTest {
|
||||
// With 200 points and interval distance of 3000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
whenAndThen(3000, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,12 +95,10 @@ public class IntervalStatisticsTest {
|
||||
// With 1000 points and interval distance of 3000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(1000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
whenAndThen(1000, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,16 +109,21 @@ public class IntervalStatisticsTest {
|
||||
// With 10000 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(10000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
whenAndThen(10000, distanceInterval);
|
||||
}
|
||||
|
||||
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(distanceInterval));
|
||||
private void whenAndThen(int numberOfPoints, float distanceInterval) {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0));
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics trackStatistics = trackWithStats.second;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
}
|
||||
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
||||
Distance totalDistance = Distance.of(0);
|
||||
float totalTime = 0L;
|
||||
@@ -129,7 +138,7 @@ public class IntervalStatisticsTest {
|
||||
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
|
||||
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
|
||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||
assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
<when>2021-05-29T18:06:22.042Z</when>
|
||||
|
||||
<when>2021-05-29T18:06:22.192Z</when>
|
||||
<when>2021-05-29T18:06:22.318Z</when>
|
||||
<when>2021-05-29T18:06:22.512Z</when>
|
||||
|
||||
<gx:coord>14.001 3.0 10.0</gx:coord>
|
||||
<gx:coord>14.002 3.0 10.0</gx:coord>
|
||||
<gx:coord />
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:22.042Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track />
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="end"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="waypoint"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<description><![CDATA[]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -46,13 +46,14 @@ limitations under the License.
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<application
|
||||
android:name=".Startup"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ThemeCustom">
|
||||
|
||||
@@ -30,8 +30,7 @@ public class Startup extends Application {
|
||||
// Set default values of preferences on first start.
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.resetPreferences(this, false);
|
||||
if (PreferencesUtils.getString(sharedPreferences, this, R.string.stats_units_key, "").equals("")) {
|
||||
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
|
||||
PreferencesUtils.setString(sharedPreferences, this, R.string.stats_units_key, statsUnits);
|
||||
PreferencesUtils.setMetricUnits(sharedPreferences, this, !Locale.US.equals(Locale.getDefault()));
|
||||
}
|
||||
|
||||
ActivityUtils.applyNightMode(sharedPreferences, this);
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
||||
@@ -64,7 +63,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
public static final String VIEW_TRACK_ICON = "track_icon";
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
|
||||
@@ -252,7 +250,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
@Override
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
super.onTrackDeleteStatus(deleteStatus);
|
||||
Log.e("probando", "trackId: " + trackId);
|
||||
if (deleteStatus.isDeleted(trackId)) {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
@@ -273,18 +270,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||
|
||||
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId != null) {
|
||||
// Use the trackId from the marker
|
||||
Marker marker = contentProviderUtils.getMarker(markerId);
|
||||
if (marker == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
trackId = marker.getTrackId();
|
||||
}
|
||||
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||
finish();
|
||||
@@ -304,7 +289,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
case 0:
|
||||
return StatisticsRecordedFragment.newInstance(trackId);
|
||||
case 1:
|
||||
return IntervalsFragment.newInstance(true);
|
||||
return IntervalsFragment.newInstance(trackId, true);
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
|
||||
@@ -360,7 +360,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
case 0:
|
||||
return StatisticsRecordingFragment.newInstance();
|
||||
case 1:
|
||||
return IntervalsFragment.newInstance(false);
|
||||
return IntervalsFragment.newInstance(trackId, false);
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
|
||||
@@ -72,10 +72,6 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
|
||||
}
|
||||
|
||||
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
|
||||
if (intervalList == data && this.metricUnits == metricUnits && this.isReportSpeed == isReportSpeed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.metricUnits = metricUnits;
|
||||
this.isReportSpeed = isReportSpeed;
|
||||
intervalList = data;
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
@@ -114,7 +115,7 @@ public class ChartView extends View {
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
private final boolean chartByDistance;
|
||||
private boolean chartByDistance = false;
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
@@ -189,9 +190,9 @@ public class ChartView extends View {
|
||||
}
|
||||
});
|
||||
|
||||
public ChartView(Context context, final boolean chartByDistance) {
|
||||
super(context);
|
||||
this.chartByDistance = chartByDistance;
|
||||
public ChartView(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
|
||||
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
||||
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
||||
|
||||
@@ -366,6 +367,10 @@ public class ChartView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setChartByDistance(boolean chartByDistance) {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
public boolean getMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
@@ -71,13 +72,12 @@ public class TrackDataHub {
|
||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final TrackDataManager trackDataManager;
|
||||
private final Set<TrackDataListener> listeners;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final int targetNumPoints;
|
||||
|
||||
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
||||
|
||||
private boolean started;
|
||||
//TODO Check if this is needed.
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
@@ -100,24 +100,23 @@ public class TrackDataHub {
|
||||
private ContentObserver trackPointsTableObserver;
|
||||
|
||||
public TrackDataHub(Context context) {
|
||||
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
this(context, new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
private TrackDataHub(Context context, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
this.context = context;
|
||||
this.trackDataManager = trackDataManager;
|
||||
this.listeners = new HashSet<>();
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.targetNumPoints = targetNumPoints;
|
||||
resetSamplingState();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (started) {
|
||||
if (isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
@@ -127,7 +126,7 @@ public class TrackDataHub {
|
||||
tracksTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
|
||||
@@ -135,7 +134,7 @@ public class TrackDataHub {
|
||||
markersTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
|
||||
@@ -143,20 +142,18 @@ public class TrackDataHub {
|
||||
trackPointsTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
||||
notifyTrackPointsTableUpdate(true, listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!started) {
|
||||
if (!isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = false;
|
||||
|
||||
//Unregister listeners
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
contentResolver.unregisterContentObserver(tracksTableObserver);
|
||||
@@ -187,10 +184,10 @@ public class TrackDataHub {
|
||||
*
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
|
||||
public void registerTrackDataListener(final TrackDataListener trackDataListener) {
|
||||
handler.post(() -> {
|
||||
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
|
||||
if (started) {
|
||||
listeners.add(trackDataListener);
|
||||
if (isStarted()) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
});
|
||||
@@ -202,7 +199,7 @@ public class TrackDataHub {
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
|
||||
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
|
||||
handler.post(() -> listeners.remove(trackDataListener));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,17 +221,17 @@ public class TrackDataHub {
|
||||
*/
|
||||
private void loadDataForAll() {
|
||||
resetSamplingState();
|
||||
if (!trackDataManager.hasListeners()) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
|
||||
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyTrackPointsTableUpdate(true, listeners);
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,25 +242,19 @@ public class TrackDataHub {
|
||||
private void loadDataForListener(TrackDataListener trackDataListener) {
|
||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||
|
||||
if (trackDataManager.listensForTracks(trackDataListener)) {
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Track
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
|
||||
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
|
||||
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
|
||||
if (hasSampledIn || hasSampledOut) {
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.emptySet();
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
|
||||
//TrackPoints
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = listeners.size() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners);
|
||||
|
||||
if (trackDataManager.listensForMarkers(trackDataListener)) {
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Markers
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,11 +307,9 @@ public class TrackDataHub {
|
||||
* Notifies track points table update; to be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param updateSamplingState true to update the sampling state
|
||||
* @param sampledInListeners the sampled-in listeners
|
||||
* @param sampledOutListeners the sampled-out listeners
|
||||
*/
|
||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
|
||||
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
|
||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +317,7 @@ public class TrackDataHub {
|
||||
// Reload and resample the track at a lower frequency.
|
||||
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
|
||||
resetSamplingState();
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
}
|
||||
@@ -353,14 +342,16 @@ public class TrackDataHub {
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = null;
|
||||
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater; //TODO This prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
|
||||
if (currentUpdater == null) {
|
||||
Log.e(TAG, "notifyTrackPointsTableUpdate() while being stopped.");
|
||||
return;
|
||||
}
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (!isStarted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
|
||||
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater;
|
||||
|
||||
trackPoint = trackPointIterator.next();
|
||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||
|
||||
@@ -384,11 +375,11 @@ public class TrackDataHub {
|
||||
|
||||
// Also include the last point if the selected track is not recording.
|
||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
||||
}
|
||||
} else {
|
||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
@@ -407,7 +398,7 @@ public class TrackDataHub {
|
||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||
}
|
||||
|
||||
sampledInListeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
|
||||
listeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
|
||||
}
|
||||
|
||||
private void correctAltitude(TrackPoint trackPoint) {
|
||||
@@ -438,6 +429,10 @@ public class TrackDataHub {
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return handlerThread != null;
|
||||
}
|
||||
|
||||
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
|
||||
this.recordingStatus = recordingStatus;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public interface TrackDataListener {
|
||||
*
|
||||
* @param marker the marker
|
||||
*/
|
||||
default void onNewMarker(Marker marker) {
|
||||
default void onNewMarker(@NonNull Marker marker) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class TrackDataManager {
|
||||
|
||||
private static final String TAG = TrackDataManager.class.getSimpleName();
|
||||
|
||||
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerMarkers = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
|
||||
|
||||
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean markersTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
|
||||
if (tracksTable) dataListenerTracks.add(trackDataListener);
|
||||
if (markersTable) dataListenerMarkers.add(trackDataListener);
|
||||
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
|
||||
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
|
||||
}
|
||||
|
||||
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
|
||||
dataListenerTracks.remove(trackDataListener);
|
||||
dataListenerMarkers.remove(trackDataListener);
|
||||
dataListenerTrackPoints_SampledIn.remove(trackDataListener);
|
||||
dataListenerTrackPoints_SampledOut.remove(trackDataListener);
|
||||
}
|
||||
|
||||
boolean hasListeners() {
|
||||
return dataListenerTracks.size() + dataListenerMarkers.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
|
||||
}
|
||||
|
||||
int getNumberOfListeners() {
|
||||
Set<TrackDataListener> listener = new HashSet<>();
|
||||
listener.addAll(dataListenerTracks);
|
||||
listener.addAll(dataListenerMarkers);
|
||||
listener.addAll(dataListenerTrackPoints_SampledIn);
|
||||
listener.addAll(dataListenerTrackPoints_SampledOut);
|
||||
|
||||
return listener.size();
|
||||
}
|
||||
|
||||
boolean listensForTracks(TrackDataListener listener) {
|
||||
return dataListenerTracks.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForMarkers(TrackDataListener listener) {
|
||||
return dataListenerMarkers.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
|
||||
return dataListenerTrackPoints_SampledIn.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
|
||||
return dataListenerTrackPoints_SampledOut.contains(listener);
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTracks() {
|
||||
return dataListenerTracks;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerMarkers() {
|
||||
return dataListenerMarkers;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
|
||||
return dataListenerTrackPoints_SampledIn;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
|
||||
return dataListenerTrackPoints_SampledOut;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public abstract class Altitude {
|
||||
|
||||
private final double altitude_m;
|
||||
@@ -12,12 +14,18 @@ public abstract class Altitude {
|
||||
return altitude_m;
|
||||
}
|
||||
|
||||
public abstract int getLabelId();
|
||||
|
||||
public static class WGS84 extends Altitude {
|
||||
|
||||
private WGS84(double altitude_m) {
|
||||
super(altitude_m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLabelId() {
|
||||
return R.string.wgs84;
|
||||
}
|
||||
|
||||
public static Altitude of(double altitude_m) {
|
||||
return new WGS84(altitude_m);
|
||||
@@ -30,6 +38,10 @@ public abstract class Altitude {
|
||||
super(altitude_m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLabelId() {
|
||||
return R.string.egm2008;
|
||||
}
|
||||
|
||||
public static Altitude of(double altitude_m) {
|
||||
return new EGM2008(altitude_m);
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class Marker {
|
||||
private String description = "";
|
||||
private String category = "";
|
||||
private String icon = "";
|
||||
private final Track.Id trackId;
|
||||
private Track.Id trackId;
|
||||
|
||||
private final Instant time;
|
||||
private Double latitude;
|
||||
@@ -74,16 +74,13 @@ public final class Marker {
|
||||
if (!trackPoint.hasLocation())
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
this.latitude = trackPoint.getLatitude();
|
||||
this.longitude = trackPoint.getLongitude();
|
||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
|
||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||
setTrackPoint(trackPoint);
|
||||
|
||||
this.length = Distance.of(0); //TODO Not cool!
|
||||
this.duration = Duration.ofMillis(0); //TODO Not cool!
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
@@ -95,6 +92,15 @@ public final class Marker {
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
//TODO Is somehow part of the initialization process. Can we at least limit visibility?
|
||||
public void setTrackPoint(TrackPoint trackPoint) {
|
||||
this.latitude = trackPoint.getLatitude();
|
||||
this.longitude = trackPoint.getLongitude();
|
||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
|
||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the it was not loaded from the database.
|
||||
*/
|
||||
@@ -143,16 +149,19 @@ public final class Marker {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Track.Id getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setTrackId(@NonNull Track.Id trackId) {
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null || longitude != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO Do not default initialize attributes; might be confusing for debugging
|
||||
public class Track {
|
||||
|
||||
private Track.Id id;
|
||||
@@ -46,19 +47,6 @@ public class Track {
|
||||
|
||||
private TrackStatistics trackStatistics = new TrackStatistics();
|
||||
|
||||
public Track() {
|
||||
}
|
||||
|
||||
public Track(Track track) {
|
||||
this.id = track.id;
|
||||
this.uuid = track.uuid;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
this.icon = icon;
|
||||
this.trackStatistics = trackStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the track was not loaded from the database.
|
||||
*/
|
||||
|
||||
@@ -48,14 +48,16 @@ public class TrackPoint {
|
||||
|
||||
private TrackPoint.Id id;
|
||||
|
||||
@NonNull
|
||||
private Instant time;
|
||||
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Float accuracy;
|
||||
private Altitude altitude;
|
||||
private Speed speed;
|
||||
private Float bearing;
|
||||
private Distance sensorDistance_m;
|
||||
private Distance sensorDistance;
|
||||
|
||||
public enum Type {
|
||||
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
||||
@@ -96,10 +98,20 @@ public class TrackPoint {
|
||||
|
||||
public TrackPoint(@NonNull Type type) {
|
||||
this.type = type;
|
||||
this.time = Instant.now(); //TODO This may result in #800
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, Instant time) {
|
||||
this(type);
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Location location) {
|
||||
this(Type.TRACKPOINT);
|
||||
this(Type.TRACKPOINT, location, Instant.now()); //TODO This may result in #800
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
|
||||
this(type);
|
||||
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
@@ -107,12 +119,9 @@ public class TrackPoint {
|
||||
this.speed = Speed.of(location.getSpeed());
|
||||
this.accuracy = location.getAccuracy();
|
||||
|
||||
setTime(Instant.now());
|
||||
}
|
||||
//TODO Should we copy the bearing?
|
||||
|
||||
public TrackPoint(@NonNull Type type, Instant time) {
|
||||
this(type);
|
||||
this.time = time;
|
||||
setTime(time);
|
||||
}
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
|
||||
@@ -198,6 +207,7 @@ public class TrackPoint {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
@@ -314,10 +324,14 @@ public class TrackPoint {
|
||||
this.accuracy = horizontalAccuracy;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (hasSensorDistance()) {
|
||||
return getSensorDistance();
|
||||
}
|
||||
if (!(hasLocation() && previous.hasLocation())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Distance.of(getLocation().distanceTo(previous.getLocation()));
|
||||
}
|
||||
@@ -326,25 +340,27 @@ public class TrackPoint {
|
||||
return hasAccuracy() && accuracy < poorAccuracy;
|
||||
}
|
||||
|
||||
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
return getLocation().bearingTo(dest.getLocation());
|
||||
}
|
||||
|
||||
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||
public float bearingTo(@NonNull Location dest) {
|
||||
return getLocation().bearingTo(dest);
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
public boolean hasSensorDistance() {
|
||||
return sensorDistance_m != null;
|
||||
return sensorDistance != null;
|
||||
}
|
||||
|
||||
public Distance getSensorDistance() {
|
||||
return sensorDistance_m;
|
||||
return sensorDistance;
|
||||
}
|
||||
|
||||
public void setSensorDistance(Distance distance_m) {
|
||||
this.sensorDistance_m = distance_m;
|
||||
this.sensorDistance = distance_m;
|
||||
}
|
||||
|
||||
public boolean hasSensorData() {
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
@@ -243,6 +244,7 @@ public class ContentProviderUtils {
|
||||
* @param track the track
|
||||
* @return the content provider URI of the inserted track.
|
||||
*/
|
||||
///TODO Return Track.Id
|
||||
public Uri insertTrack(Track track) {
|
||||
return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
|
||||
}
|
||||
@@ -569,6 +571,7 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
//TODO Only used for file import; might be better to replace it.
|
||||
//TODO Rename to bulkInsert
|
||||
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
|
||||
ContentValues[] values = new ContentValues[trackPoints.size()];
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
@@ -577,6 +580,15 @@ public class ContentProviderUtils {
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||
}
|
||||
|
||||
//TODO Set trackId in this method.
|
||||
public int bulkInsertMarkers(List<Marker> markers, Track.Id trackId) {
|
||||
ContentValues[] values = new ContentValues[markers.size()];
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
values[i] = createContentValues(markers.get(i));
|
||||
}
|
||||
return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last location id for a track.
|
||||
* Returns -1L if it doesn't exist.
|
||||
|
||||
@@ -15,8 +15,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors.
|
||||
* <p>
|
||||
* https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
|
||||
*/
|
||||
public final class SensorDataCycling {
|
||||
|
||||
@@ -96,7 +94,7 @@ public final class SensorDataCycling {
|
||||
|
||||
public static class DistanceSpeed extends SensorData<DistanceSpeed.Data> {
|
||||
|
||||
private final Integer wheelRevolutionsCount; // UINT16
|
||||
private final Long wheelRevolutionsCount; // UINT32
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
|
||||
public DistanceSpeed(String sensorAddress) {
|
||||
@@ -105,7 +103,7 @@ public final class SensorDataCycling {
|
||||
this.wheelRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public DistanceSpeed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||
public DistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.wheelRevolutionsCount = wheelRevolutionsCount;
|
||||
this.wheelRevolutionsTime = wheelRevolutionsTime;
|
||||
@@ -115,7 +113,7 @@ public final class SensorDataCycling {
|
||||
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
|
||||
}
|
||||
|
||||
public int getWheelRevolutionsCount() {
|
||||
public long getWheelRevolutionsCount() {
|
||||
return wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
@@ -128,20 +126,20 @@ public final class SensorDataCycling {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
if (timeDiff.isZero() || timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
|
||||
value = null;
|
||||
} else {
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
|
||||
|
||||
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||
Distance distanceOverall = distance;
|
||||
if (previous.hasValue()) {
|
||||
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
value = new Data(distance, distanceOverall, speed_mps);
|
||||
return;
|
||||
}
|
||||
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
|
||||
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||
Distance distanceOverall = distance;
|
||||
if (previous.hasValue()) {
|
||||
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
value = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +161,11 @@ public final class SensorDataCycling {
|
||||
if (!(obj instanceof DistanceSpeed)) return false;
|
||||
|
||||
DistanceSpeed comp = (DistanceSpeed) obj;
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
} else {
|
||||
if (!(hasData() && comp.hasData())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
|
||||
@@ -16,28 +16,25 @@
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -68,7 +65,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private Distance recordingDistanceInterval;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
@@ -80,40 +76,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
private boolean chartByDistance;
|
||||
|
||||
private ChartBinding viewBinding;
|
||||
// UI elements
|
||||
private ChartView chartView;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
if (metricUnits != chartView.getMetricUnits()) {
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
|
||||
viewBinding.chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
viewBinding.chartView.requestLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||
if (reportSpeed != chartView.getReportSpeed()) {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
chartView.applyReportSpeed();
|
||||
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||
viewBinding.chartView.applyReportSpeed();
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
viewBinding.chartView.requestLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, getContext());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,12 +113,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
private final Runnable updateChart = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || trackDataHub == null) {
|
||||
if (!isResumed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
chartView.setShowPointer(isSelectedTrackRecording());
|
||||
chartView.invalidate();
|
||||
viewBinding.chartView.setShowPointer(isSelectedTrackRecording());
|
||||
viewBinding.chartView.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,27 +128,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true);
|
||||
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
|
||||
|
||||
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
|
||||
chartView = new ChartView(getContext(), chartByDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = ChartBinding.inflate(inflater, container, false);
|
||||
viewBinding.chartView.setChartByDistance(chartByDistance);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
viewBinding.chartViewLayout.addView(chartView, layoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -178,13 +156,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
viewBinding.chartViewLayout.removeView(chartView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -200,16 +171,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
if (track == null || track.getTrackStatistics() == null) {
|
||||
if (track == null) {
|
||||
category = "";
|
||||
return;
|
||||
}
|
||||
|
||||
category = track.getCategory();
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
|
||||
if (reportSpeed != chartView.getReportSpeed()) {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
chartView.applyReportSpeed();
|
||||
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||
viewBinding.chartView.applyReportSpeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,10 +189,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
pendingPoints.clear();
|
||||
chartView.reset();
|
||||
viewBinding.chartView.reset();
|
||||
runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
chartView.resetScroll();
|
||||
viewBinding.chartView.resetScroll();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,7 +200,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
|
||||
if (isResumed()) {
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, chartView.getMetricUnits());
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, viewBinding.chartView.getMetricUnits());
|
||||
pendingPoints.add(point);
|
||||
}
|
||||
}
|
||||
@@ -237,7 +208,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
chartView.addChartPoints(pendingPoints);
|
||||
//Avoid ConcurrentModificationException exception
|
||||
viewBinding.chartView.addChartPoints(Collections.unmodifiableList(pendingPoints));
|
||||
pendingPoints.clear();
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
@@ -246,14 +218,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void clearMarkers() {
|
||||
if (isResumed()) {
|
||||
chartView.clearMarker();
|
||||
viewBinding.chartView.clearMarker();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewMarker(Marker marker) {
|
||||
if (isResumed() && marker != null) {
|
||||
chartView.addMarker(marker);
|
||||
public void onNewMarker(@NonNull Marker marker) {
|
||||
if (isResumed()) {
|
||||
viewBinding.chartView.addMarker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,9 +240,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
* Checks the chart settings.
|
||||
*/
|
||||
private void checkChartSettings() {
|
||||
boolean needUpdate = chartView.applyReportSpeed();
|
||||
boolean needUpdate = viewBinding.chartView.applyReportSpeed();
|
||||
if (needUpdate) {
|
||||
chartView.postInvalidate();
|
||||
viewBinding.chartView.postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +252,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, true, true, true, true);
|
||||
trackDataHub.registerTrackDataListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,14 +280,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
private void runOnUiThread(Runnable runnable) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
Activity fragmentActivity = getActivity();
|
||||
if (fragmentActivity != null) {
|
||||
fragmentActivity.runOnUiThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setChartView(ChartView view) {
|
||||
chartView = view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -20,15 +19,10 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
|
||||
@@ -36,31 +30,31 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
|
||||
/**
|
||||
* A fragment to display the intervals from recorded track.
|
||||
*/
|
||||
public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
public class IntervalsFragment extends Fragment {
|
||||
|
||||
private static final String TAG = IntervalsFragment.class.getSimpleName();
|
||||
|
||||
private static final String FROM_TOP_TO_BOTTOM_KEY = "fromTopToBottom";
|
||||
private static final String TRACK_ID_KEY = "trackId";
|
||||
|
||||
private IntervalStatisticsModel viewModel;
|
||||
protected IntervalStatisticsAdapter.StackMode stackModeListView;
|
||||
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
||||
|
||||
private Track.Id trackId;
|
||||
private boolean metricUnits;
|
||||
private IntervalStatisticsAdapter adapter;
|
||||
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
private boolean isReportSpeed;
|
||||
|
||||
private IntervalListViewBinding viewBinding;
|
||||
|
||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
uploadIntervals();
|
||||
updateIntervals(PreferencesUtils.isMetricUnits(sharedPreferences, getContext()), selectedInterval);
|
||||
if (spinnerAdapter != null) {
|
||||
spinnerAdapter.notifyDataSetChanged();
|
||||
}
|
||||
@@ -70,12 +64,14 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param trackId
|
||||
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
|
||||
* @return IntervalsFragment instance.
|
||||
*/
|
||||
public static Fragment newInstance(boolean fromTopToBottom) {
|
||||
public static Fragment newInstance(Track.Id trackId, boolean fromTopToBottom) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
|
||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
||||
IntervalsFragment intervalsFragment = new IntervalsFragment();
|
||||
intervalsFragment.setArguments(bundle);
|
||||
return intervalsFragment;
|
||||
@@ -85,6 +81,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
stackModeListView = getArguments().getBoolean(FROM_TOP_TO_BOTTOM_KEY, true) ? IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP : IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
|
||||
trackId = getArguments().getParcelable(TRACK_ID_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,8 +124,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
viewBinding.spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
|
||||
uploadIntervals();
|
||||
updateIntervals(metricUnits, IntervalStatisticsModel.IntervalOption.values()[i]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,18 +136,27 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getContext());
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
if (track != null) {
|
||||
isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory());
|
||||
}
|
||||
|
||||
viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class);
|
||||
loadIntervals();
|
||||
resumeTrackDataHub();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
if (viewModel != null) {
|
||||
viewModel.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,88 +182,18 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
if (viewModel == null) {
|
||||
return;
|
||||
}
|
||||
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(metricUnits, selectedInterval);
|
||||
viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace));
|
||||
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, metricUnits, selectedInterval);
|
||||
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
|
||||
}
|
||||
|
||||
private synchronized void uploadIntervals() {
|
||||
if (viewModel == null) {
|
||||
return;
|
||||
}
|
||||
viewModel.upload(metricUnits, selectedInterval);
|
||||
}
|
||||
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
|
||||
boolean update = metricUnits != this.metricUnits || !selectedInterval.sameMultiplier(this.selectedInterval);
|
||||
this.metricUnits = metricUnits;
|
||||
this.selectedInterval = selectedInterval;
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, true, false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
trackDataHub = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
// Set category.
|
||||
String category = track != null ? track.getCategory() : "";
|
||||
|
||||
// Set rate label.
|
||||
isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||
viewBinding.intervalRate.setText(isReportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
viewModel.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused, Speed unused2, double unused3) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
runOnUiThread(viewModel::onNewTrackPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a runnable on the UI thread.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
private void runOnUiThread(Runnable runnable) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
if (fragmentActivity != null) {
|
||||
fragmentActivity.runOnUiThread(runnable);
|
||||
if (update && viewModel != null) {
|
||||
viewModel.update(trackId, this.metricUnits, this.selectedInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +276,18 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
|
||||
if (preferenceShowAltitude) {
|
||||
// Current altitude
|
||||
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
|
||||
Float altitude = null;
|
||||
int labelId = R.string.value_unknown;
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasAltitude()) {
|
||||
altitude = (float) latestTrackPoint.getAltitude().toM();
|
||||
labelId = latestTrackPoint.getAltitude().getLabelId();
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
|
||||
|
||||
viewBinding.statsAltitudeCurrentLabelEgm.setText(labelId);
|
||||
}
|
||||
|
||||
// Set coordinate
|
||||
@@ -313,7 +321,7 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
|
||||
}
|
||||
|
||||
this.latestTrackPoint = recordingData.getLatestTrackPoint();
|
||||
latestTrackPoint = recordingData.getLatestTrackPoint();
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasLocation() && !latestTrackPoint.isRecent()) {
|
||||
latestTrackPoint = null;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
*/
|
||||
public enum TrackFileFormat {
|
||||
|
||||
@Deprecated //Not needed anymore; we now have OSMDashboard; was only used to share with other applications
|
||||
KML_ONLY_TRACK {
|
||||
@Override
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
|
||||
@@ -198,7 +198,6 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
|
||||
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
|
||||
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
||||
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
|
||||
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
|
||||
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
|
||||
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
/**
|
||||
* Abstract class for file track importers.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
abstract class AbstractFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
||||
|
||||
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final Distance recordingDistanceInterval;
|
||||
|
||||
private final List<Track.Id> trackIds = new ArrayList<>();
|
||||
private final List<Marker> markers = new ArrayList<>();
|
||||
|
||||
// The current element content
|
||||
//TODO Should be made private and getter be used by child classes.
|
||||
protected String content;
|
||||
|
||||
protected String icon;
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected String category;
|
||||
protected String latitude;
|
||||
protected String longitude;
|
||||
protected String altitude;
|
||||
protected String time;
|
||||
protected String speed;
|
||||
protected String heartrate;
|
||||
protected String cadence;
|
||||
protected String power;
|
||||
protected String markerType;
|
||||
protected String photoUrl;
|
||||
protected String uuid;
|
||||
protected String gain;
|
||||
protected String loss;
|
||||
protected String distance;
|
||||
|
||||
// The current track data
|
||||
private TrackData trackData;
|
||||
|
||||
// The SAX locator to get the current line information
|
||||
private Locator locator;
|
||||
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
||||
this.context = context;
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] ch, int start, int length) {
|
||||
String newContent = new String(ch, start, length);
|
||||
if (content == null) {
|
||||
content = newContent;
|
||||
} else {
|
||||
// In 99% of the cases, a single call to this method will be made for each sequence of characters we're interested in, so we'll rarely be concatenating strings, thus not justifying the use of a StringBuilder.
|
||||
content += newContent;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onFileEnd() {
|
||||
// Add markers to the last imported track
|
||||
int size = trackIds.size();
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
Track.Id trackId = trackIds.get(size - 1);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int markerPosition = -1;
|
||||
Marker marker = null;
|
||||
TrackPoint trackPoint = null;
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
|
||||
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
|
||||
while (true) {
|
||||
if (marker == null) {
|
||||
markerPosition++;
|
||||
marker = markerPosition < markers.size() ? markers.get(markerPosition) : null;
|
||||
if (marker == null) {
|
||||
// No more markers
|
||||
return;
|
||||
}
|
||||
// If marker had photo it must be translated to internal photo url (depend on track id)
|
||||
if (marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(getInternalPhotoUrl(marker.getPhotoUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint == null) {
|
||||
if (!trackPointIterator.hasNext()) {
|
||||
// No more track points. Ignore the rest of the markers.
|
||||
return;
|
||||
}
|
||||
trackPoint = trackPointIterator.next();
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
}
|
||||
|
||||
if ((marker.getTime()).isAfter(trackPoint.getTime())) {
|
||||
trackPoint = null;
|
||||
} else if (marker.getTime().isBefore(trackPoint.getTime())) {
|
||||
Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
|
||||
marker = null;
|
||||
} else {
|
||||
// The marker trackPoint time matches the track point time
|
||||
if (!trackPoint.hasLocation()) {
|
||||
// Invalid trackPoint, load the next trackPoint
|
||||
trackPoint = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid trackPoint
|
||||
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
|
||||
String markerDescription = marker.getDescription();
|
||||
String icon = context.getString(R.string.marker_icon_url);
|
||||
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
|
||||
|
||||
// Insert marker
|
||||
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
|
||||
contentProviderUtils.insertMarker(newMarker);
|
||||
}
|
||||
|
||||
// Load the next marker
|
||||
marker = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On track start.
|
||||
*/
|
||||
protected void onTrackStart() throws SAXException {
|
||||
trackData = new TrackData();
|
||||
}
|
||||
|
||||
/**
|
||||
* On track end.
|
||||
*/
|
||||
protected void onTrackEnd() {
|
||||
if (name != null) {
|
||||
trackData.track.setName(name);
|
||||
}
|
||||
|
||||
try {
|
||||
trackData.track.setUuid(UUID.fromString(uuid));
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
Log.w(TAG, "could not parse Track UUID, generating a new one.");
|
||||
trackData.track.setUuid(UUID.randomUUID());
|
||||
}
|
||||
|
||||
if (description != null) {
|
||||
trackData.track.setDescription(description);
|
||||
}
|
||||
if (category != null) {
|
||||
trackData.track.setCategory(category);
|
||||
//TODO remove when GPX and KML support reading this property.
|
||||
if (icon == null)
|
||||
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
|
||||
}
|
||||
if (icon != null) {
|
||||
trackData.track.setIcon(icon);
|
||||
}
|
||||
|
||||
TrackStatisticsUpdater statistics = new TrackStatisticsUpdater();
|
||||
//TODO I guess, we should not filter by recordingDistanceInterval on import; the data is already recorded, so we should not change it.
|
||||
for (TrackPoint trackPoint : trackData.bufferedTrackPoints) {
|
||||
statistics.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
}
|
||||
if (!statistics.isTrackInitialized()) {
|
||||
throw new ImportParserException("Track did not contain any locations.");
|
||||
}
|
||||
trackData.track.setTrackStatistics(statistics.getTrackStatistics());
|
||||
|
||||
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
|
||||
if (track != null) {
|
||||
if (PreferencesUtils.getPreventReimportTracks(sharedPreferences, context)) {
|
||||
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
|
||||
}
|
||||
|
||||
//TODO This is a workaround until we have proper UI.
|
||||
trackData.track.setUuid(UUID.randomUUID());
|
||||
}
|
||||
|
||||
if (trackIds.size() > 0) {
|
||||
// TODO Multi track is not supported yet.
|
||||
cleanImport();
|
||||
throw new ImportParserException("Multi track not supported");
|
||||
}
|
||||
Uri uri = contentProviderUtils.insertTrack(trackData.track);
|
||||
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
|
||||
trackIds.add(trackId);
|
||||
trackData.track.setId(trackId);
|
||||
|
||||
flushTrackPoints();
|
||||
}
|
||||
|
||||
protected void onTrackSegmentStart() {
|
||||
trackData.numberOfSegments++;
|
||||
|
||||
//If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
|
||||
if (trackData.numberOfSegments > 1
|
||||
&& trackData.lastLocationInCurrentSegment != null
|
||||
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL))
|
||||
) {
|
||||
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
|
||||
}
|
||||
trackData.lastLocationInCurrentSegment = null;
|
||||
}
|
||||
|
||||
protected void onTrackSegmentEnd() {
|
||||
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
|
||||
if (trackPoint == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackPoint.hasLocation()) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addMarker() throws ParsingException {
|
||||
// Markers must have a time, else cannot match to the track points
|
||||
if (time == null) {
|
||||
Log.w(TAG, "Marker without time ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (!trackPoint.hasLocation()) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
||||
return;
|
||||
}
|
||||
Marker marker = new Marker(null, trackPoint);
|
||||
|
||||
if (name != null) {
|
||||
marker.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
marker.setDescription(description);
|
||||
}
|
||||
if (category != null) {
|
||||
marker.setCategory(category);
|
||||
}
|
||||
|
||||
if (photoUrl != null) {
|
||||
marker.setPhotoUrl(photoUrl);
|
||||
}
|
||||
markers.add(marker);
|
||||
}
|
||||
|
||||
protected TrackPoint getTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
Instant time = trackPoint.getTime();
|
||||
if (trackPoint.getLatitude() == 100) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
trackPoint.setTime(time);
|
||||
} else if (trackPoint.getLatitude() == 200) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
|
||||
trackPoint.setTime(time);
|
||||
|
||||
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate derived attributes from the previous point
|
||||
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() == null) {
|
||||
if (!trackPoint.hasSpeed()) {
|
||||
Duration timeDifference = Duration.between(trackData.lastLocationInCurrentSegment.getTime(), trackPoint.getTime());
|
||||
|
||||
// Check for negative time change
|
||||
if (timeDifference.isNegative()) {
|
||||
Log.w(TAG, "Time difference not positive.");
|
||||
} else {
|
||||
|
||||
/*
|
||||
* We don't have a speed and bearing in GPX, make something up from the last two points.
|
||||
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
|
||||
*/
|
||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
||||
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
||||
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
|
||||
|
||||
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
|
||||
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
|
||||
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trackData.lastLocationInCurrentSegment = trackPoint;
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error message.
|
||||
*
|
||||
* @param message the message
|
||||
*/
|
||||
protected String createErrorMessage(String message) {
|
||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the photo url for a file.
|
||||
*
|
||||
* @param externalPhotoUrl the file name
|
||||
*/
|
||||
protected String getInternalPhotoUrl(String externalPhotoUrl) {
|
||||
if (trackData.track.getId() == null) {
|
||||
Log.e(TAG, "Track id is invalid.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (externalPhotoUrl == null) {
|
||||
Log.i(TAG, "External photo url is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
|
||||
File file = FileUtils.buildInternalPhotoFile(context, trackData.track.getId(), Uri.parse(importFileName));
|
||||
if (file != null) {
|
||||
Uri photoUri = FileUtils.getUriForFile(context, file);
|
||||
return "" + photoUri;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected TrackPoint createTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
try {
|
||||
trackPoint.setTime(StringUtils.parseTime(time));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
try {
|
||||
trackPoint.setLatitude(Double.parseDouble(latitude));
|
||||
trackPoint.setLongitude(Double.parseDouble(longitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
|
||||
if (altitude != null) {
|
||||
try {
|
||||
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||
}
|
||||
}
|
||||
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a trackPoint.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
protected void insertTrackPoint(TrackPoint trackPoint) {
|
||||
trackData.bufferedTrackPoints.add(trackPoint);
|
||||
}
|
||||
|
||||
protected boolean isFirstTrackPointInSegment() {
|
||||
return trackData.lastLocationInCurrentSegment == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the TrackPoints to the database.
|
||||
*/
|
||||
private void flushTrackPoints() {
|
||||
if (trackData.bufferedTrackPoints.size() > 0) {
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
|
||||
trackData.bufferedTrackPoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Track.Id> getImportTrackIds() {
|
||||
return trackIds;
|
||||
}
|
||||
|
||||
public void cleanImport() {
|
||||
contentProviderUtils.deleteTracks(context, trackIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for the current track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
// TODO Why private inner class?
|
||||
private static class TrackData {
|
||||
// The current track
|
||||
final Track track = new Track();
|
||||
|
||||
// The number of segments processed for the current track
|
||||
int numberOfSegments = 0;
|
||||
|
||||
// The last location in the current segment; Null if the current segment doesn't have a last location
|
||||
TrackPoint lastLocationInCurrentSegment;
|
||||
|
||||
// The buffered locations
|
||||
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
|
||||
}
|
||||
|
||||
public static class ParsingException extends RuntimeException {
|
||||
|
||||
private ParsingException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
protected ParsingException(@NonNull String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,19 +17,25 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Imports a GPX file.
|
||||
@@ -43,7 +49,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
public class GpxTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
||||
|
||||
private static final String TAG = GpxTrackImporter.class.getSimpleName();
|
||||
|
||||
private static final String TAG_DESCRIPTION = "desc";
|
||||
private static final String TAG_COMMENT = "cmt";
|
||||
@@ -70,23 +78,46 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
|
||||
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public GpxFileTrackImporter(Context context) {
|
||||
this(context, new ContentProviderUtils(context));
|
||||
}
|
||||
private Locator locator;
|
||||
|
||||
@VisibleForTesting
|
||||
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
||||
super(context, contentProviderUtils);
|
||||
private final Context context;
|
||||
|
||||
// Belongs to the current track
|
||||
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
// The current element content
|
||||
private String content = "";
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private String category;
|
||||
private String latitude;
|
||||
private String longitude;
|
||||
private String altitude;
|
||||
private String time;
|
||||
private String speed;
|
||||
private String heartrate;
|
||||
private String cadence;
|
||||
private String power;
|
||||
protected String markerType;
|
||||
protected String photoUrl;
|
||||
protected String uuid;
|
||||
protected String gain;
|
||||
protected String loss;
|
||||
protected String distance;
|
||||
|
||||
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
|
||||
|
||||
private final TrackImporter trackImporter;
|
||||
|
||||
public GpxTrackImporter(Context context, TrackImporter trackImporter) {
|
||||
this.context = context;
|
||||
this.trackImporter = trackImporter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,7 +127,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerStart(attributes);
|
||||
break;
|
||||
case TAG_TRACK:
|
||||
onTrackStart();
|
||||
trackImporter.newTrack();
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
onTrackSegmentStart();
|
||||
@@ -107,6 +138,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] ch, int start, int length) {
|
||||
content += new String(ch, start, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String tag) {
|
||||
switch (tag) {
|
||||
@@ -117,13 +153,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerEnd();
|
||||
break;
|
||||
case TAG_TRACK:
|
||||
onTrackEnd();
|
||||
trackImporter.setTrack(context, name, uuid, description, category, null);
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_TRACK_POINT:
|
||||
onTrackPointEnd();
|
||||
currentSegment.add(createTrackPoint());
|
||||
break;
|
||||
case TAG_NAME:
|
||||
if (content != null) {
|
||||
@@ -197,33 +233,65 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
content = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackStart() throws SAXException {
|
||||
super.onTrackStart();
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
private void onTrackSegmentStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackPoint createTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = super.createTrackPoint();
|
||||
private void onTrackSegmentEnd() {
|
||||
if (currentSegment.isEmpty()) {
|
||||
Log.w(TAG, "No locations in current segment.");
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint first = currentSegment.getFirst();
|
||||
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
|
||||
trackImporter.addTrackPoints(currentSegment);
|
||||
currentSegment.clear();
|
||||
}
|
||||
|
||||
|
||||
private TrackPoint createTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
try {
|
||||
trackPoint.setTime(StringUtils.parseTime(time));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
try {
|
||||
trackPoint.setLatitude(Double.parseDouble(latitude));
|
||||
trackPoint.setLongitude(Double.parseDouble(longitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
|
||||
if (altitude != null) {
|
||||
try {
|
||||
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (speed != null) {
|
||||
try {
|
||||
trackPoint.setSpeed(Speed.of(speed));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
|
||||
}
|
||||
}
|
||||
if (heartrate != null) {
|
||||
try {
|
||||
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||
}
|
||||
}
|
||||
@@ -239,7 +307,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (power != null) {
|
||||
try {
|
||||
trackPoint.setPower(Float.parseFloat(power));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
|
||||
}
|
||||
}
|
||||
@@ -247,21 +315,21 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (gain != null) {
|
||||
try {
|
||||
trackPoint.setAltitudeGain(Float.parseFloat(gain));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
|
||||
}
|
||||
}
|
||||
if (loss != null) {
|
||||
try {
|
||||
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
|
||||
}
|
||||
}
|
||||
if (distance != null) {
|
||||
try {
|
||||
trackPoint.setSensorDistance(Distance.of(distance));
|
||||
} catch (Exception e) {
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
|
||||
}
|
||||
}
|
||||
@@ -269,11 +337,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point start.
|
||||
*
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
private void onTrackPointStart(Attributes attributes) {
|
||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||
@@ -285,15 +348,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
loss = null;
|
||||
}
|
||||
|
||||
private void onTrackPointEnd() {
|
||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
||||
TrackPoint trackPoint = getTrackPoint();
|
||||
if (isFirstTrackPointInSegment) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
insertTrackPoint(trackPoint);
|
||||
}
|
||||
|
||||
private void onMarkerStart(Attributes attributes) {
|
||||
name = null;
|
||||
description = null;
|
||||
@@ -307,6 +361,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
|
||||
private void onMarkerEnd() {
|
||||
addMarker();
|
||||
// Markers must have a time, else cannot match to the track points
|
||||
if (time == null) {
|
||||
Log.w(TAG, "Marker without time; ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (!trackPoint.hasLocation()) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
||||
return;
|
||||
}
|
||||
Marker marker = new Marker(null, trackPoint);
|
||||
|
||||
if (name != null) {
|
||||
marker.setName(name);
|
||||
}
|
||||
if (description != null) {
|
||||
marker.setDescription(description);
|
||||
}
|
||||
if (category != null) {
|
||||
marker.setCategory(category);
|
||||
}
|
||||
|
||||
if (photoUrl != null) {
|
||||
marker.setPhotoUrl(photoUrl);
|
||||
}
|
||||
markers.add(marker);
|
||||
}
|
||||
|
||||
private String createErrorMessage(String message) {
|
||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||
}
|
||||
|
||||
private void onFileEnd() {
|
||||
trackImporter.addMarkers(markers);
|
||||
trackImporter.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Track.Id> getImportTrackIds() {
|
||||
return trackImporter.getTrackIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanImport() {
|
||||
trackImporter.cleanImport();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.io.file.importer;
|
||||
import android.app.job.JobService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
@@ -16,9 +17,12 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
public class ImportService extends JobIntentService {
|
||||
|
||||
@@ -50,13 +54,19 @@ public class ImportService extends JobIntentService {
|
||||
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
try {
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, this);
|
||||
Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, this);
|
||||
boolean preventReimport = PreferencesUtils.getPreventReimportTracks(sharedPreferences, this);
|
||||
|
||||
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport);
|
||||
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new GpxFileTrackImporter(this)).importFile(this, file.getUri()));
|
||||
trackIds.addAll(new XMLImporter(new GpxTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new KmlFileTrackImporter(this)).importFile(this, file.getUri()));
|
||||
trackIds.addAll(new XMLImporter(new KmlTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new KmzTrackImporter().importFile(this, file.getUri()));
|
||||
trackIds.addAll(new KmzTrackImporter(this, trackImporter).importFile(file.getUri()));
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file format.");
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
|
||||
|
||||
@@ -17,30 +17,35 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
public class KmlTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
||||
|
||||
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
|
||||
private static final String TAG = KmlTrackImporter.class.getSimpleName();
|
||||
|
||||
private static final String MARKER_STYLE = "#" + KMLTrackExporter.MARKER_STYLE;
|
||||
|
||||
@@ -64,29 +69,49 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private static final String ATTRIBUTE_NAME = "name";
|
||||
|
||||
private boolean trackStarted = false;
|
||||
private Locator locator;
|
||||
|
||||
private final Context context;
|
||||
|
||||
// Belongs to the current track
|
||||
private final ArrayList<Instant> whenList = new ArrayList<>();
|
||||
private final ArrayList<Location> locationList = new ArrayList<>();
|
||||
|
||||
private String extendedDataType;
|
||||
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private final ArrayList<Float> speedList = new ArrayList<>();
|
||||
private final ArrayList<Float> distanceList = new ArrayList<>();
|
||||
private final ArrayList<Float> cadenceList = new ArrayList<>();
|
||||
private final ArrayList<Float> heartRateList = new ArrayList<>();
|
||||
private final ArrayList<Float> powerList = 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> sensorPowerList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
|
||||
|
||||
public KmlFileTrackImporter(Context context) {
|
||||
this(context, new ContentProviderUtils(context));
|
||||
}
|
||||
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
@VisibleForTesting
|
||||
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
||||
super(context, contentProviderUtils);
|
||||
// The current element content
|
||||
private String content = "";
|
||||
|
||||
private String icon;
|
||||
private String name;
|
||||
private String description;
|
||||
private String category;
|
||||
private String latitude;
|
||||
private String longitude;
|
||||
private String altitude;
|
||||
private String markerType;
|
||||
private String photoUrl;
|
||||
private String uuid;
|
||||
|
||||
private final TrackImporter trackImporter;
|
||||
|
||||
public KmlTrackImporter(Context context, TrackImporter trackImporter) {
|
||||
this.context = context;
|
||||
this.trackImporter = trackImporter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,11 +123,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerStart();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
trackStarted = true;
|
||||
onTrackStart();
|
||||
trackImporter.newTrack();
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
if (!trackStarted) {
|
||||
if (trackImporter == null) {
|
||||
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
||||
}
|
||||
onTrackSegmentStart();
|
||||
@@ -113,6 +137,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] ch, int start, int length) {
|
||||
content += new String(ch, start, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String tag) throws SAXException {
|
||||
switch (tag) {
|
||||
@@ -128,13 +157,13 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerLocationEnd();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
onTrackEnd();
|
||||
trackImporter.setTrack(context, name, uuid, description, category, icon);
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_GX_COORD:
|
||||
onTrackPointEnd();
|
||||
onCoordEnded();
|
||||
break;
|
||||
case TAG_GX_VALUE:
|
||||
onExtendedDataValueEnd();
|
||||
@@ -166,7 +195,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
break;
|
||||
case TAG_WHEN:
|
||||
if (content != null) {
|
||||
time = content.trim();
|
||||
whenList.add(StringUtils.parseTime(content.trim()));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -183,7 +212,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
content = "";
|
||||
}
|
||||
|
||||
private void onMarkerStart() {
|
||||
@@ -196,7 +225,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
latitude = null;
|
||||
longitude = null;
|
||||
altitude = null;
|
||||
time = null;
|
||||
markerType = null;
|
||||
}
|
||||
|
||||
@@ -204,7 +232,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (!MARKER_STYLE.equals(markerType)) {
|
||||
return;
|
||||
}
|
||||
addMarker();
|
||||
|
||||
if (whenList.size() != 1) {
|
||||
Log.w(TAG, "Marker without time ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = createLocation(longitude, latitude, altitude);
|
||||
if (location == null) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + location);
|
||||
return;
|
||||
}
|
||||
|
||||
Marker marker = new Marker(null, new TrackPoint(TrackPoint.Type.TRACKPOINT, location, whenList.get(0))); //TODO Creating marker without need
|
||||
marker.setName(name != null ? name : "");
|
||||
marker.setDescription(description != null ? description : "");
|
||||
marker.setCategory(category != null ? category : "");
|
||||
marker.setPhotoUrl(photoUrl);
|
||||
markers.add(marker);
|
||||
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
photoUrl = null;
|
||||
whenList.clear();
|
||||
}
|
||||
|
||||
private void onMarkerLocationEnd() {
|
||||
@@ -219,39 +270,56 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackSegmentStart() {
|
||||
super.onTrackSegmentStart();
|
||||
trackPoints.clear();
|
||||
speedList.clear();
|
||||
distanceList.clear();
|
||||
heartRateList.clear();
|
||||
cadenceList.clear();
|
||||
powerList.clear();
|
||||
private void onTrackSegmentStart() {
|
||||
locationList.clear();
|
||||
whenList.clear();
|
||||
|
||||
sensorSpeedList.clear();
|
||||
sensorDistanceList.clear();
|
||||
sensorHeartRateList.clear();
|
||||
sensorCadenceList.clear();
|
||||
sensorPowerList.clear();
|
||||
altitudeGainList.clear();
|
||||
altitudeLossList.clear();
|
||||
}
|
||||
|
||||
protected void onTrackSegmentEnd() {
|
||||
super.onTrackSegmentEnd();
|
||||
// Close a track segment by inserting the segment locations
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
private void onTrackSegmentEnd() {
|
||||
if (locationList.size() != whenList.size()) {
|
||||
throw new ImportParserException("<coords> and <when> should have the same count.");
|
||||
}
|
||||
|
||||
if (i < speedList.size() && speedList.get(i) != null) {
|
||||
trackPoint.setSpeed(Speed.of(speedList.get(i)));
|
||||
// Close a track segment by inserting the segment locations
|
||||
for (int i = 0; i < locationList.size(); i++) {
|
||||
Instant time = whenList.get(i);
|
||||
Location location = locationList.get(i);
|
||||
|
||||
TrackPoint trackPoint;
|
||||
if (i == 0) {
|
||||
if (location == null) {
|
||||
trackPoint = TrackPoint.createSegmentStartManualWithTime(time);
|
||||
} else {
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time);
|
||||
}
|
||||
} else if (i == locationList.size() - 1 && location == null) {
|
||||
trackPoint = TrackPoint.createSegmentEndWithTime(time);
|
||||
} else {
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
|
||||
}
|
||||
if (i < distanceList.size() && distanceList.get(i) != null) {
|
||||
trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
|
||||
|
||||
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
|
||||
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
|
||||
}
|
||||
if (i < heartRateList.size()) {
|
||||
trackPoint.setHeartRate_bpm(heartRateList.get(i));
|
||||
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
|
||||
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
|
||||
}
|
||||
if (i < cadenceList.size()) {
|
||||
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
|
||||
if (i < sensorHeartRateList.size()) {
|
||||
trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i));
|
||||
}
|
||||
if (i < powerList.size()) {
|
||||
trackPoint.setPower(powerList.get(i));
|
||||
if (i < sensorCadenceList.size()) {
|
||||
trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i));
|
||||
}
|
||||
if (i < sensorPowerList.size()) {
|
||||
trackPoint.setPower(sensorPowerList.get(i));
|
||||
}
|
||||
if (i < altitudeGainList.size()) {
|
||||
trackPoint.setAltitudeGain(altitudeGainList.get(i));
|
||||
@@ -260,18 +328,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
|
||||
}
|
||||
|
||||
insertTrackPoint(trackPoint);
|
||||
trackImporter.addTrackPoint(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point end. gx:coord end tag.
|
||||
*/
|
||||
private void onTrackPointEnd() {
|
||||
// Add trackPoint to trackPoints
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
private void onCoordEnded() {
|
||||
String[] parts = content.trim().split(" ");
|
||||
if (parts.length == 2 || parts.length == 3) {
|
||||
longitude = parts[0];
|
||||
@@ -279,33 +340,41 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
}
|
||||
|
||||
// Similar to GPX
|
||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
||||
TrackPoint trackPoint = getTrackPoint();
|
||||
if (isFirstTrackPointInSegment) {
|
||||
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
|
||||
locationList.add(createLocation(longitude, latitude, altitude));
|
||||
|
||||
trackPoint.setType(type);
|
||||
}
|
||||
trackPoints.add(trackPoint);
|
||||
|
||||
// Reset variables for next trackpoint (which might not have such data).
|
||||
time = null;
|
||||
longitude = null;
|
||||
latitude = null;
|
||||
altitude = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On extended data start. gx:SimpleArrayData start tag.
|
||||
*/
|
||||
private Location createLocation(String longitude, String latitude, String altitude) {
|
||||
Location location = null;
|
||||
if (longitude != null || latitude != null) {
|
||||
location = new Location("import");
|
||||
|
||||
try {
|
||||
location.setLatitude(Double.parseDouble(latitude));
|
||||
location.setLongitude(Double.parseDouble(longitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
|
||||
if (altitude != null) {
|
||||
try {
|
||||
location.setAltitude(Double.parseDouble(altitude));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
private void onExtendedDataStart(Attributes attributes) {
|
||||
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* On extended data value end. gx:value end tag.
|
||||
*/
|
||||
private void onExtendedDataValueEnd() throws SAXException {
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
@@ -320,19 +389,19 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
switch (extendedDataType) {
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
||||
speedList.add(value);
|
||||
sensorSpeedList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
|
||||
distanceList.add(value);
|
||||
sensorDistanceList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
|
||||
powerList.add(value);
|
||||
sensorPowerList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
|
||||
heartRateList.add(value);
|
||||
sensorHeartRateList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
|
||||
cadenceList.add(value);
|
||||
sensorCadenceList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
|
||||
altitudeGainList.add(value);
|
||||
@@ -344,4 +413,28 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private String createErrorMessage(String message) {
|
||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||
}
|
||||
|
||||
private void onFileEnd() {
|
||||
trackImporter.addMarkers(markers);
|
||||
trackImporter.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Track.Id> getImportTrackIds() {
|
||||
return trackImporter.getTrackIds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanImport() {
|
||||
trackImporter.cleanImport();
|
||||
}
|
||||
}
|
||||
@@ -51,11 +51,16 @@ public class KmzTrackImporter {
|
||||
|
||||
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private final TrackImporter trackImporter;
|
||||
|
||||
public KmzTrackImporter(Context context, TrackImporter trackImporter) {
|
||||
this.context = context;
|
||||
this.trackImporter = trackImporter;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
|
||||
this.context = context;
|
||||
public List<Track.Id> importFile(Uri fileUri) throws IOException {
|
||||
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
|
||||
|
||||
List<Track.Id> trackIdsWithImages = new ArrayList<>();
|
||||
@@ -205,7 +210,7 @@ public class KmzTrackImporter {
|
||||
}
|
||||
|
||||
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
|
||||
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
|
||||
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ParsingException extends RuntimeException {
|
||||
|
||||
private ParsingException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
protected ParsingException(@NonNull String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
/**
|
||||
* Handles logic to import:
|
||||
* 1. addTrackPoints()
|
||||
* 2. addMarkers();
|
||||
* 3. setTrack();
|
||||
* 4. newTrack(); //stores current track to databse
|
||||
* 5. if needed go to 1.
|
||||
* 6. finish()
|
||||
* <p>
|
||||
* NOTE: This class modifies the parameter.
|
||||
* Do not re-use these objects anywhere else.
|
||||
*/
|
||||
public class TrackImporter {
|
||||
|
||||
private static final String TAG = TrackImporter.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private final Distance recordingDistanceInterval;
|
||||
private final Distance maxRecordingDistance;
|
||||
private final boolean preventReimport;
|
||||
|
||||
private final List<Track.Id> trackIds = new ArrayList<>();
|
||||
|
||||
// Current track
|
||||
private Track track;
|
||||
private final List<TrackPoint> trackPoints = new LinkedList<>();
|
||||
private final List<Marker> markers = new LinkedList<>();
|
||||
|
||||
public TrackImporter(Context context, ContentProviderUtils contentProviderUtils, Distance recordingDistanceInterval, Distance maxRecordingDistance, boolean preventReimport) {
|
||||
this.context = context;
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.recordingDistanceInterval = recordingDistanceInterval;
|
||||
this.maxRecordingDistance = maxRecordingDistance;
|
||||
this.preventReimport = preventReimport;
|
||||
}
|
||||
|
||||
void newTrack() {
|
||||
if (track != null) {
|
||||
finishTrack();
|
||||
}
|
||||
|
||||
track = null;
|
||||
trackPoints.clear();
|
||||
markers.clear();
|
||||
}
|
||||
|
||||
void addTrackPoint(TrackPoint trackPoint) {
|
||||
this.trackPoints.add(trackPoint);
|
||||
}
|
||||
|
||||
void addTrackPoints(List<TrackPoint> trackPoints) {
|
||||
this.trackPoints.addAll(trackPoints);
|
||||
}
|
||||
|
||||
void addMarker(Marker marker) {
|
||||
this.markers.add(marker);
|
||||
}
|
||||
|
||||
void addMarkers(List<Marker> markers) {
|
||||
this.markers.addAll(markers);
|
||||
}
|
||||
|
||||
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
|
||||
track = new Track();
|
||||
track.setName(name != null ? name : "");
|
||||
|
||||
try {
|
||||
track.setUuid(UUID.fromString(uuid));
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
Log.w(TAG, "could not parse Track UUID, generating a new one.");
|
||||
track.setUuid(UUID.randomUUID());
|
||||
}
|
||||
|
||||
track.setDescription(description != null ? description : "");
|
||||
|
||||
if (category != null) {
|
||||
track.setCategory(category);
|
||||
|
||||
if (icon == null) {
|
||||
icon = TrackIconUtils.getIconValue(context, category);
|
||||
}
|
||||
}
|
||||
|
||||
track.setIcon(icon != null ? icon : "");
|
||||
}
|
||||
|
||||
void finish() {
|
||||
if (track != null) {
|
||||
finishTrack();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishTrack() {
|
||||
if (trackPoints.isEmpty()) {
|
||||
throw new ImportParserException("Cannot import track without any locations.");
|
||||
}
|
||||
|
||||
// Store Track
|
||||
if (contentProviderUtils.getTrack(track.getUuid()) != null) {
|
||||
if (preventReimport) {
|
||||
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
|
||||
}
|
||||
|
||||
//TODO This is a workaround until we have proper UI.
|
||||
track.setUuid(UUID.randomUUID());
|
||||
}
|
||||
|
||||
Collections.sort(trackPoints, new Comparator<TrackPoint>() {
|
||||
@Override
|
||||
public int compare(TrackPoint o1, TrackPoint o2) {
|
||||
if (o1.getTime().isBefore(o2.getTime())) {
|
||||
return -1;
|
||||
}
|
||||
if (o1.getTime().isAfter(o2.getTime())) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||
updater.addTrackPoints(trackPoints, recordingDistanceInterval);
|
||||
track.setTrackStatistics(updater.getTrackStatistics());
|
||||
|
||||
Uri trackUri = contentProviderUtils.insertTrack(track);
|
||||
Track.Id trackId = new Track.Id(ContentUris.parseId(trackUri));
|
||||
|
||||
// Store TrackPoints
|
||||
adjustTrackPoints();
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
|
||||
|
||||
// Store Markers
|
||||
matchMarkers2TrackPoints(trackId);
|
||||
for (Marker marker : markers)
|
||||
marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers
|
||||
|
||||
contentProviderUtils.bulkInsertMarkers(markers, trackId);
|
||||
|
||||
//Clear up.
|
||||
trackPoints.clear();
|
||||
markers.clear();
|
||||
|
||||
trackIds.add(trackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* If not present: calculate data from the previous trackPoint (if present)
|
||||
* NOTE: Modifies content of trackPoints.
|
||||
*/
|
||||
private void adjustTrackPoints() {
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
TrackPoint current = trackPoints.get(i);
|
||||
|
||||
if (current.hasLocation()) {
|
||||
Instant time = current.getTime();
|
||||
if (current.getLatitude() == 100) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, time));
|
||||
} else if (current.getLatitude() == 200) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time));
|
||||
//TODO Delete location
|
||||
} else if (!LocationUtils.isValidLocation(current.getLocation())) {
|
||||
throw new ImportParserException("Invalid location detected: " + current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < trackPoints.size(); i++) {
|
||||
TrackPoint previous = trackPoints.get(i - 1);
|
||||
TrackPoint current = trackPoints.get(i);
|
||||
|
||||
Distance distanceToPrevious = current.distanceToPrevious(previous);
|
||||
if (distanceToPrevious != null) {
|
||||
if (!current.hasSpeed()) {
|
||||
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
|
||||
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
|
||||
}
|
||||
|
||||
if (!current.hasBearing()) {
|
||||
current.setBearing(previous.bearingTo(current));
|
||||
}
|
||||
|
||||
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Modifies content of markers (incl. removal).
|
||||
*/
|
||||
private void matchMarkers2TrackPoints(Track.Id trackId) {
|
||||
List<TrackPoint> trackPointsWithLocation = trackPoints.stream()
|
||||
.filter(TrackPoint::hasLocation)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Marker> todoMarkers = new LinkedList<>(markers);
|
||||
List<Marker> doneMarkers = new LinkedList<>();
|
||||
|
||||
for (final TrackPoint trackPoint : trackPointsWithLocation) {
|
||||
if (todoMarkers.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||
updater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
|
||||
List<Marker> matchedMarkers = todoMarkers.stream()
|
||||
.filter(it -> trackPoint.getLatitude() == it.getLatitude()
|
||||
&& trackPoint.getLongitude() == it.getLongitude()
|
||||
&& trackPoint.getTime().equals(it.getTime())
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
TrackStatistics statistics = updater.getTrackStatistics();
|
||||
for (Marker marker : matchedMarkers) {
|
||||
if (marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
|
||||
}
|
||||
|
||||
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
|
||||
|
||||
marker.setLength(statistics.getTotalDistance());
|
||||
marker.setDuration(statistics.getTotalTime());
|
||||
|
||||
marker.setTrackPoint(trackPoint);
|
||||
}
|
||||
|
||||
todoMarkers.removeAll(matchedMarkers);
|
||||
doneMarkers.addAll(matchedMarkers);
|
||||
}
|
||||
|
||||
if (todoMarkers.isEmpty()) {
|
||||
Log.w(TAG, "Some markers could not be attached to TrackPoints; those are not imported.");
|
||||
}
|
||||
|
||||
markers.clear();
|
||||
markers.addAll(doneMarkers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the photo url for a file.
|
||||
*
|
||||
* @param externalPhotoUrl the file name
|
||||
*/
|
||||
private String getInternalPhotoUrl(@NonNull Track.Id trackId, @NonNull String externalPhotoUrl) {
|
||||
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
|
||||
File file = FileUtils.buildInternalPhotoFile(context, trackId, Uri.parse(importFileName));
|
||||
if (file != null) {
|
||||
Uri photoUri = FileUtils.getUriForFile(context, file);
|
||||
return "" + photoUri;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Track.Id> getTrackIds() {
|
||||
return Collections.unmodifiableList(trackIds);
|
||||
}
|
||||
|
||||
public void cleanImport() {
|
||||
contentProviderUtils.deleteTracks(context, trackIds);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class XMLImporter {
|
||||
try {
|
||||
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
|
||||
return parser.getImportTrackIds();
|
||||
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
|
||||
} catch (SAXException | ParserConfigurationException | ParsingException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
if (parser.getImportTrackIds().size() > 0) {
|
||||
parser.cleanImport();
|
||||
@@ -58,6 +58,7 @@ public class XMLImporter {
|
||||
}
|
||||
|
||||
interface TrackParser {
|
||||
@Deprecated
|
||||
DefaultHandler getHandler();
|
||||
|
||||
List<Track.Id> getImportTrackIds();
|
||||
|
||||
@@ -36,12 +36,14 @@ import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -59,6 +61,7 @@ import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.EGM2008Utils;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
@@ -128,6 +131,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
private BluetoothRemoteSensorManager remoteSensorManager;
|
||||
private AltitudeSumManager altitudeSumManager;
|
||||
|
||||
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
||||
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater;
|
||||
private TrackPoint lastTrackPoint;
|
||||
private boolean isIdle;
|
||||
@@ -539,6 +544,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
|
||||
fillWithSensorDataSet(trackPoint);
|
||||
|
||||
correctAltitude(trackPoint);
|
||||
|
||||
notificationManager.updateTrackPoint(this, track.getTrackStatistics(), trackPoint, recordingGpsAccuracy);
|
||||
|
||||
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
|
||||
@@ -602,6 +609,25 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
lastTrackPoint = trackPoint;
|
||||
}
|
||||
|
||||
//TODO Identical to TrackDataHub
|
||||
private void correctAltitude(TrackPoint trackPoint) {
|
||||
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
|
||||
Log.d(TAG, "No altitude correction necessary.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (egm2008Correction == null || !egm2008Correction.canCorrect(trackPoint.getLocation())) {
|
||||
try {
|
||||
egm2008Correction = EGM2008Utils.createCorrection(this, trackPoint.getLocation());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Could not load altitude correction for " + trackPoint, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
|
||||
notificationManager.updateContent(getString(gpsStatusValue.message));
|
||||
@@ -652,8 +678,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
recordingDataObservable.postValue(new RecordingData(track, trackPoint, sensorDataSet));
|
||||
|
||||
contentProviderUtils.updateTrack(track);
|
||||
} catch (SQLiteException e) {
|
||||
/*
|
||||
@@ -670,7 +694,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
return null;
|
||||
}
|
||||
|
||||
SensorDataSet sensorData = remoteSensorManager.getSensorData();
|
||||
SensorDataSet sensorData = remoteSensorManager.getSensorData(); //TODO Should return a copy of SensorDataSet
|
||||
if (sensorData == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -756,10 +780,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
return;
|
||||
}
|
||||
Track track = contentProviderUtils.getTrack(recordingStatus.getTrackId());
|
||||
track.getTrackStatistics().setTotalTime(TrackRecordingService.this.getTotalTime());
|
||||
|
||||
SensorDataSet sensorDataSet = fillWithSensorDataSet(lastTrackPoint);
|
||||
recordingDataObservable.postValue(new RecordingData(track, lastTrackPoint, sensorDataSet));
|
||||
// Compute temporary track statistics using sensorData and update time.
|
||||
//TODO This somehow should happen in the HandlerServer as we create a new TrackPoint.
|
||||
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
|
||||
TrackPoint tmpLastTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
|
||||
//TODO Should happen in TrackPoint? via constructor
|
||||
tmpLastTrackPoint.setSpeed(lastTrackPoint.getSpeed());
|
||||
tmpLastTrackPoint.setAltitude(lastTrackPoint.getAltitude());
|
||||
tmpLastTrackPoint.setLongitude(lastTrackPoint.getLongitude());
|
||||
tmpLastTrackPoint.setLatitude(lastTrackPoint.getLatitude());
|
||||
}
|
||||
|
||||
SensorDataSet sensorDataSet = fillWithSensorDataSet(tmpLastTrackPoint);
|
||||
|
||||
tmpTrackStatisticsUpdater.addTrackPoint(tmpLastTrackPoint, recordingDistanceInterval);
|
||||
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
recordingDataObservable.postValue(new RecordingData(track, tmpLastTrackPoint, sensorDataSet));
|
||||
}
|
||||
|
||||
public LiveData<RecordingStatus> getRecordingStatusObservable() {
|
||||
|
||||
@@ -25,13 +25,11 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
@@ -47,11 +45,6 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
*/
|
||||
public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
private static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
@@ -178,17 +171,13 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
}
|
||||
String category = track.getCategory();
|
||||
|
||||
//TODO Querying all TrackPoints all the time is inefficient; get infos from TrackRecordingService
|
||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
||||
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
while (trackPointIterator.hasNext()) {
|
||||
trackPoints.add(trackPointIterator.next());
|
||||
}
|
||||
|
||||
boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
|
||||
boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category);
|
||||
Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
|
||||
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.one(isMetricUnits));
|
||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance);
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
|
||||
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval);
|
||||
@@ -217,10 +206,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
*/
|
||||
}
|
||||
tts.setLanguage(locale);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
|
||||
tts.setSpeechRate(PreferencesUtils.getVoiceSpeedRate(PreferencesUtils.getSharedPreferences(context), context));
|
||||
tts.setOnUtteranceProgressListener(utteranceListener);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,16 +76,10 @@ final class PreferenceHelper {
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = context.getString(R.string.value_float_mile, mile);
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
}
|
||||
@@ -118,24 +112,14 @@ final class PreferenceHelper {
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = context.getString(R.string.value_float_mile, mile);
|
||||
if (value == recordingGPSAccuracyPoor) {
|
||||
entries[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
|
||||
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
|
||||
|
||||
private Preference voiceAnnouncements;
|
||||
private Preference announcementsFrequency;
|
||||
private Preference announcementsSpeed;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -140,7 +141,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
ttsInitStatus = status;
|
||||
Log.i(TAG, "TextToSpeech initialized with status " + status);
|
||||
|
||||
if (voiceAnnouncements != null) {
|
||||
if (announcementsFrequency != null && announcementsSpeed != null) {
|
||||
updateVoiceAnnouncements();
|
||||
}
|
||||
});
|
||||
@@ -172,7 +173,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
return directory != null ? directory.getName() : getString(R.string.not_set);
|
||||
});
|
||||
|
||||
voiceAnnouncements = findPreference(getString(R.string.voice_frequency_key));
|
||||
announcementsFrequency = findPreference(getString(R.string.voice_frequency_key));
|
||||
announcementsSpeed = findPreference(getString(R.string.voice_speed_rate_key));
|
||||
updateVoiceAnnouncements();
|
||||
}
|
||||
|
||||
@@ -232,7 +234,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection.unbind(getContext());
|
||||
sharedPreferences = null;
|
||||
voiceAnnouncements = null;
|
||||
announcementsFrequency = null;
|
||||
announcementsSpeed = null;
|
||||
tts = null;
|
||||
}
|
||||
|
||||
@@ -323,7 +326,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
}
|
||||
|
||||
private void updateVoiceAnnouncements() {
|
||||
voiceAnnouncements.setEnabled(ttsInitStatus == TextToSpeech.SUCCESS);
|
||||
boolean enabled = ttsInitStatus == TextToSpeech.SUCCESS;
|
||||
announcementsFrequency.setEnabled(enabled);
|
||||
announcementsSpeed.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
|
||||
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.stats;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This class maintains a ring buffer of doubles.
|
||||
* This buffer is a convenient class for storing a series of doubles and calculating information about them.
|
||||
@@ -49,6 +51,12 @@ class DoubleRingBuffer {
|
||||
reset();
|
||||
}
|
||||
|
||||
DoubleRingBuffer(DoubleRingBuffer toCopy) {
|
||||
this.buffer = Arrays.copyOf(toCopy.buffer, toCopy.buffer.length);
|
||||
this.index = toCopy.index;
|
||||
this.isFull = toCopy.isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer.
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
@@ -63,18 +64,18 @@ public class TrackStatisticsUpdater {
|
||||
|
||||
private final TrackStatistics trackStatistics;
|
||||
|
||||
private final DoubleRingBuffer altitudeBuffer_m = new DoubleRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
|
||||
private final DoubleRingBuffer speedBuffer_mps = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
|
||||
private final DoubleRingBuffer altitudeBuffer_m;
|
||||
private final DoubleRingBuffer speedBuffer_mps;
|
||||
|
||||
// The current segment's statistics
|
||||
private final TrackStatistics currentSegment = new TrackStatistics();
|
||||
private final TrackStatistics currentSegment;
|
||||
// Current segment's last trackPoint
|
||||
private TrackPoint lastTrackPoint;
|
||||
// Current segment's last moving trackPoint
|
||||
private TrackPoint lastMovingTrackPoint;
|
||||
|
||||
public TrackStatisticsUpdater() {
|
||||
trackStatistics = new TrackStatistics();
|
||||
this(new TrackStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +85,25 @@ public class TrackStatisticsUpdater {
|
||||
*/
|
||||
public TrackStatisticsUpdater(TrackStatistics trackStatistics) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.currentSegment = new TrackStatistics();
|
||||
|
||||
trackInitialized = true;
|
||||
|
||||
altitudeBuffer_m = new DoubleRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
|
||||
speedBuffer_mps = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
|
||||
}
|
||||
|
||||
public TrackStatisticsUpdater(TrackStatisticsUpdater toCopy) {
|
||||
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
|
||||
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
|
||||
|
||||
this.trackInitialized = toCopy.trackInitialized;
|
||||
this.segmentInitialized = toCopy.segmentInitialized;
|
||||
this.altitudeBuffer_m = new DoubleRingBuffer(toCopy.altitudeBuffer_m);
|
||||
this.speedBuffer_mps = new DoubleRingBuffer(toCopy.speedBuffer_mps);
|
||||
|
||||
this.lastTrackPoint = toCopy.lastTrackPoint;
|
||||
this.lastMovingTrackPoint = toCopy.lastMovingTrackPoint;
|
||||
}
|
||||
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
@@ -98,18 +117,16 @@ public class TrackStatisticsUpdater {
|
||||
return trackInitialized;
|
||||
}
|
||||
|
||||
public void addTrackPoints(List<TrackPoint> trackPoints, Distance minGPSDistance) {
|
||||
for (TrackPoint tp : trackPoints) {
|
||||
addTrackPoint(tp, minGPSDistance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a trackPoint.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
* @param minGPSDistance the min recording distance
|
||||
*/
|
||||
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
|
||||
internalAddTrackPoint(trackPoint, minGPSDistance);
|
||||
Log.v(TAG, this.toString());
|
||||
}
|
||||
|
||||
private void internalAddTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
|
||||
if (!trackInitialized) {
|
||||
trackStatistics.setStartTime(trackPoint.getTime());
|
||||
trackInitialized = true;
|
||||
@@ -157,13 +174,14 @@ public class TrackStatisticsUpdater {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackPoint.hasSensorDistance()) {
|
||||
if (!trackPoint.hasSensorDistance()
|
||||
&& trackPoint.hasLocation() && lastMovingTrackPoint.hasLocation()) {
|
||||
// GPS-based distance/speed
|
||||
Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
|
||||
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
|
||||
if (movingDistance != null && movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
|
||||
speedBuffer_mps.reset();
|
||||
lastTrackPoint = trackPoint;
|
||||
return;
|
||||
return; //TOOD Why? Is there nothing to be done afterwards?
|
||||
}
|
||||
// Update total distance
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
|
||||
@@ -96,6 +96,22 @@ public class PreferencesUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static float getFloat(SharedPreferences sharedPreferences, Context context, int keyId, float defaultValue) {
|
||||
try {
|
||||
return sharedPreferences.getFloat(getKey(context, keyId), defaultValue);
|
||||
} catch (ClassCastException e) {
|
||||
//Ignore
|
||||
}
|
||||
|
||||
//NOTE: We assume that the data was stored as String due to use of ListPreference.
|
||||
try {
|
||||
String stringValue = sharedPreferences.getString(getKey(context, keyId), null);
|
||||
return Float.parseFloat(stringValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getString(SharedPreferences sharedPreferences, Context context, int keyId, String defaultValue) {
|
||||
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
@@ -112,6 +128,16 @@ public class PreferencesUtils {
|
||||
return STATS_UNIT.equals(getString(sharedPreferences, context, R.string.stats_units_key, STATS_UNIT));
|
||||
}
|
||||
|
||||
public static void setMetricUnits(SharedPreferences sharedPreferences, Context context, boolean metricUnits) {
|
||||
String unit;
|
||||
if (metricUnits) {
|
||||
unit = context.getString(R.string.stats_units_metric);
|
||||
} else {
|
||||
unit = context.getString(R.string.stats_units_imperial);
|
||||
}
|
||||
setString(sharedPreferences, context, R.string.stats_units_key, unit);
|
||||
}
|
||||
|
||||
public static boolean isReportSpeed(SharedPreferences sharedPreferences, Context context, String category) {
|
||||
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
|
||||
String currentStatsRate = getString(sharedPreferences, context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
|
||||
@@ -181,6 +207,11 @@ public class PreferencesUtils {
|
||||
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
|
||||
}
|
||||
|
||||
public static float getVoiceSpeedRate(SharedPreferences sharedPreferences, Context context) {
|
||||
final float DEFAULT = Float.parseFloat(context.getResources().getString(R.string.voice_frequency_default));
|
||||
return getFloat(sharedPreferences, context, R.string.voice_speed_rate_key, DEFAULT);
|
||||
}
|
||||
|
||||
public static Distance getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
|
||||
return Distance.of(getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefaultInternal(context)));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
@@ -96,38 +95,15 @@ public class StringUtils {
|
||||
return context.getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
if (metricUnits) {
|
||||
if (distance.greaterThan(Distance.of(500))) {
|
||||
return context.getString(R.string.value_float_kilometer, distance.toKM());
|
||||
} else {
|
||||
return context.getString(R.string.value_float_meter, distance.toM());
|
||||
}
|
||||
} else {
|
||||
if (distance.greaterThan(Distance.ofMile(0.5))) {
|
||||
return context.getString(R.string.value_float_mile, distance.toMI());
|
||||
} else {
|
||||
return context.getString(R.string.value_float_feet, distance.toFT());
|
||||
}
|
||||
}
|
||||
Pair<String, String> distanceParts = getDistanceParts(context, distance, metricUnits);
|
||||
|
||||
return context.getString(R.string.distance_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
|
||||
if (reportSpeed) {
|
||||
if (metricUnits) {
|
||||
return context.getString(R.string.value_float_kilometer_hour, speed.toKMH());
|
||||
} else {
|
||||
return context.getString(R.string.value_float_mile_hour, speed.toMPH());
|
||||
}
|
||||
} else {
|
||||
int pace = (int) speed.toPace(metricUnits).getSeconds();
|
||||
int minutes = pace / 60;
|
||||
int seconds = pace % 60;
|
||||
if (metricUnits) {
|
||||
return context.getString(R.string.value_pace_kilometer, minutes, seconds);
|
||||
} else {
|
||||
return context.getString(R.string.value_pace_mile, minutes, seconds);
|
||||
}
|
||||
}
|
||||
Pair<String, String> distanceParts = getSpeedParts(context, speed, metricUnits, reportSpeed);
|
||||
|
||||
return context.getString(R.string.speed_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
private static String formatDecimal(double value) {
|
||||
@@ -139,6 +115,7 @@ public class StringUtils {
|
||||
*/
|
||||
public static String formatDecimal(double value, int decimalPlaces) {
|
||||
DecimalFormat df = new DecimalFormat();
|
||||
df.setMinimumFractionDigits(decimalPlaces);
|
||||
df.setMaximumFractionDigits(decimalPlaces);
|
||||
df.setRoundingMode(RoundingMode.HALF_EVEN);
|
||||
return df.format(value);
|
||||
@@ -173,7 +150,7 @@ public class StringUtils {
|
||||
return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
|
||||
}
|
||||
} else {
|
||||
if (distance.greaterThan(Distance.of(0.5 * UnitConversions.M_TO_MI))) {
|
||||
if (distance.greaterThan(Distance.ofMile(0.5))) {
|
||||
return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
|
||||
@@ -211,7 +188,7 @@ public class StringUtils {
|
||||
|
||||
int minutes = pace / 60;
|
||||
int seconds = pace % 60;
|
||||
return new Pair<>(String.format(Locale.US, "%d:%02d", minutes, seconds), unitString);
|
||||
return new Pair<>(context.getString(R.string.time, minutes, seconds), unitString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +215,7 @@ public class StringUtils {
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[").append(category).append("]");
|
||||
builder.append(getCategory(category));
|
||||
if (description != null && description.length() != 0) {
|
||||
builder.append(" ").append(description);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ public class UintUtils {
|
||||
/**
|
||||
* Computes a - b for UINT with overflow (b < a).
|
||||
*
|
||||
* @return diff or -1 (invalid)
|
||||
* @return diff
|
||||
*/
|
||||
public static long diff(long a, long b, final long UINT_MAX) {
|
||||
if (a < 0 || b < 0) {
|
||||
return -1;
|
||||
throw new RuntimeException("a or b cannot be less than zero.");
|
||||
}
|
||||
if (a > UINT_MAX || b > UINT_MAX) {
|
||||
return -1;
|
||||
throw new RuntimeException("a or b are outside of the allowed range.");
|
||||
}
|
||||
|
||||
if (a >= b) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
@@ -9,45 +9,72 @@ import java.util.List;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
|
||||
public class IntervalStatistics {
|
||||
private final List<Interval> intervalList = new ArrayList<>();
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
private final List<Interval> intervalList;
|
||||
private final Distance distanceInterval;
|
||||
private final Distance minGPSDistance;
|
||||
private Interval interval, lastInterval;
|
||||
|
||||
public IntervalStatistics(@NonNull List<TrackPoint> trackPoints, Distance distanceInterval) {
|
||||
intervalList.clear();
|
||||
/**
|
||||
* @param distanceInterval distance of every interval.
|
||||
* @param minGPSDistance the setting value for GPS distance.
|
||||
*/
|
||||
public IntervalStatistics(Distance distanceInterval, Distance minGPSDistance) {
|
||||
this.distanceInterval = distanceInterval;
|
||||
this.minGPSDistance = minGPSDistance;
|
||||
|
||||
if (trackPoints.size() == 0) {
|
||||
return;
|
||||
}
|
||||
interval = new Interval();
|
||||
lastInterval = new Interval();
|
||||
intervalList = new ArrayList<>();
|
||||
intervalList.add(lastInterval);
|
||||
}
|
||||
|
||||
Interval interval = new Interval();
|
||||
interval.gain_m += trackPoints.get(0).hasAltitudeGain() ? trackPoints.get(0).getAltitudeGain() : 0;
|
||||
interval.loss_m += trackPoints.get(0).hasAltitudeLoss() ? trackPoints.get(0).getAltitudeLoss() : 0;
|
||||
for (int i = 1; i < trackPoints.size(); i++) {
|
||||
TrackPoint prevTrackPoint = trackPoints.get(i - 1);
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
/**
|
||||
* Complete intervals with the tracks points from the iterator.
|
||||
*
|
||||
* @param trackPointIterator
|
||||
* @return the last track point's id used to compute the intervals.
|
||||
*/
|
||||
public TrackPoint.Id addTrackPoints(TrackPointIterator trackPointIterator) {
|
||||
boolean newIntervalAdded = false;
|
||||
TrackPoint trackPoint = null;
|
||||
|
||||
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
|
||||
interval.distance = interval.distance.plus(prevTrackPoint.distanceToPrevious(trackPoint));
|
||||
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
|
||||
interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0;
|
||||
interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0;
|
||||
while (trackPointIterator.hasNext()) {
|
||||
trackPoint = trackPointIterator.next();
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, minGPSDistance);
|
||||
|
||||
if (interval.distance.greaterThan(distanceInterval)) {
|
||||
Interval adjustedInterval = new Interval(interval, distanceInterval.dividedBy(interval.distance));
|
||||
if (trackStatisticsUpdater.getTrackStatistics().getTotalDistance().plus(interval.distance).greaterOrEqualThan(distanceInterval)) {
|
||||
interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint);
|
||||
|
||||
intervalList.add(adjustedInterval);
|
||||
double adjustFactor = distanceInterval.dividedBy(interval.distance);
|
||||
Interval adjustedInterval = new Interval(interval);
|
||||
adjustedInterval.adjust(adjustFactor);
|
||||
|
||||
interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time));
|
||||
}
|
||||
intervalList.set(intervalList.size() - 1, adjustedInterval);
|
||||
|
||||
interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time));
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, minGPSDistance);
|
||||
|
||||
lastInterval = new Interval(interval);
|
||||
intervalList.add(lastInterval);
|
||||
|
||||
newIntervalAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (interval.distance.greaterThan(Distance.of(1))) {
|
||||
intervalList.add(interval);
|
||||
if (newIntervalAdded) {
|
||||
lastInterval.add(trackStatisticsUpdater.getTrackStatistics(), null);
|
||||
} else {
|
||||
lastInterval.set(trackStatisticsUpdater.getTrackStatistics());
|
||||
}
|
||||
|
||||
return trackPoint != null ? trackPoint.getId() : null;
|
||||
}
|
||||
|
||||
public List<Interval> getIntervalList() {
|
||||
@@ -77,8 +104,8 @@ public class IntervalStatistics {
|
||||
public static class Interval {
|
||||
private Distance distance = Distance.of(0);
|
||||
private Duration time = Duration.ofSeconds(0);
|
||||
private float gain_m = 0f;
|
||||
private float loss_m = 0f;
|
||||
private float gain_m;
|
||||
private float loss_m;
|
||||
|
||||
public Interval() {
|
||||
}
|
||||
@@ -96,6 +123,18 @@ public class IntervalStatistics {
|
||||
loss_m = i.loss_m;
|
||||
}
|
||||
|
||||
public Interval(Interval i) {
|
||||
distance = i.distance;
|
||||
time = i.time;
|
||||
gain_m = i.gain_m;
|
||||
loss_m = i.loss_m;
|
||||
}
|
||||
|
||||
private void adjust(double adjustFactor) {
|
||||
distance = distance.multipliedBy(adjustFactor);
|
||||
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
|
||||
}
|
||||
|
||||
public Distance getDistance() {
|
||||
return distance;
|
||||
}
|
||||
@@ -111,5 +150,23 @@ public class IntervalStatistics {
|
||||
public float getLoss_m() {
|
||||
return loss_m;
|
||||
}
|
||||
|
||||
private void add(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) {
|
||||
distance = distance.plus(trackStatistics.getTotalDistance());
|
||||
time = time.plus(trackStatistics.getTotalTime());
|
||||
gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m;
|
||||
loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m;
|
||||
if (lastTrackPoint != null) {
|
||||
gain_m = lastTrackPoint.hasAltitudeGain() ? gain_m - lastTrackPoint.getAltitudeGain() : gain_m;
|
||||
loss_m = lastTrackPoint.hasAltitudeLoss() ? loss_m - lastTrackPoint.getAltitudeLoss() : loss_m;
|
||||
}
|
||||
}
|
||||
|
||||
private void set(TrackStatistics trackStatistics) {
|
||||
distance = trackStatistics.getTotalDistance();
|
||||
time = trackStatistics.getTotalTime();
|
||||
gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m;
|
||||
loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* This model is used to load intervals for a track.
|
||||
@@ -19,63 +30,92 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
*/
|
||||
public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
|
||||
private final List<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private static final String TAG = IntervalStatisticsModel.class.getSimpleName();
|
||||
|
||||
private MutableLiveData<List<IntervalStatistics.Interval>> intervalsLiveData;
|
||||
private IntervalStatistics intervalStatistics;
|
||||
private Distance distanceInterval;
|
||||
private final Distance minGPSDistance;
|
||||
private final ContentResolver contentResolver;
|
||||
private ContentObserver trackPointsTableObserver;
|
||||
private TrackPoint.Id lastTrackPointId;
|
||||
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
|
||||
public IntervalStatisticsModel(@NonNull Application application) {
|
||||
super(application);
|
||||
minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(PreferencesUtils.getSharedPreferences(application), application);
|
||||
contentResolver = getApplication().getContentResolver();
|
||||
handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
synchronized (trackPoints) {
|
||||
if (intervalsLiveData == null) {
|
||||
if (interval == null) {
|
||||
interval = IntervalOption.OPTION_1;
|
||||
}
|
||||
|
||||
intervalsLiveData = new MutableLiveData<>();
|
||||
distanceInterval = interval.getDistance(metricUnits);
|
||||
loadIntervalStatistics();
|
||||
}
|
||||
return intervalsLiveData;
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
if (trackPointsTableObserver != null) {
|
||||
contentResolver.unregisterContentObserver(trackPointsTableObserver);
|
||||
trackPointsTableObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIntervalStatistics() {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
|
||||
intervalsLiveData.postValue(intervalStatistics.getIntervalList());
|
||||
}
|
||||
|
||||
public void add(TrackPoint trackPoint) {
|
||||
synchronized (trackPoints) {
|
||||
trackPoints.add(trackPoint);
|
||||
if (handlerThread != null) {
|
||||
handlerThread.getLooper().quit();
|
||||
handlerThread = null;
|
||||
}
|
||||
handler = null;
|
||||
}
|
||||
|
||||
public void onNewTrackPoints() {
|
||||
synchronized (trackPoints) {
|
||||
if (intervalsLiveData != null) {
|
||||
loadIntervalStatistics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (trackPoints) {
|
||||
trackPoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void upload(boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
synchronized (trackPoints) {
|
||||
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
if (intervalsLiveData == null) {
|
||||
if (interval == null) {
|
||||
interval = IntervalOption.OPTION_1;
|
||||
}
|
||||
|
||||
intervalsLiveData = new MutableLiveData<>();
|
||||
distanceInterval = interval.getDistance(metricUnits);
|
||||
loadIntervalStatistics();
|
||||
intervalStatistics = new IntervalStatistics(distanceInterval, minGPSDistance);
|
||||
|
||||
loadIntervalStatistics(trackId);
|
||||
}
|
||||
|
||||
trackPointsTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange, Uri uri) {
|
||||
loadIntervalStatistics(trackId);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_TRACKID, false, trackPointsTableObserver);
|
||||
|
||||
return intervalsLiveData;
|
||||
}
|
||||
|
||||
private void loadIntervalStatistics(Track.Id trackId) {
|
||||
executor.execute(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication());
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, lastTrackPointId)) {
|
||||
lastTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
intervalsLiveData.postValue(intervalStatistics.getIntervalList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onPause() {
|
||||
if (trackPointsTableObserver != null) {
|
||||
contentResolver.unregisterContentObserver(trackPointsTableObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
if (interval == null) {
|
||||
interval = IntervalOption.OPTION_1;
|
||||
}
|
||||
|
||||
lastTrackPointId = null;
|
||||
distanceInterval = interval.getDistance(metricUnits);
|
||||
intervalStatistics = new IntervalStatistics(distanceInterval, minGPSDistance);
|
||||
loadIntervalStatistics(trackId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +143,10 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
.multipliedBy(multiplier);
|
||||
}
|
||||
|
||||
public boolean sameMultiplier(IntervalOption intervalOption) {
|
||||
return intervalOption != null && this.multiplier == intervalOption.multiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
|
||||
<foreground>
|
||||
<inset
|
||||
android:inset="20dp"
|
||||
android:drawable="@drawable/ic_logo_color_24dp" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/ic_logo_color_24dp" />
|
||||
@@ -1,33 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="0.56"
|
||||
android:scaleY="0.56"
|
||||
android:translateX="21.50"
|
||||
android:translateY="24.00">
|
||||
<path
|
||||
android:pathData="M85.133,20.59C85.731,18.546 84.756,17.817 82.965,18.968L23.874,56.967c-1.791,1.152 -1.519,2.224 0.604,2.382L49.019,61.18c2.124,0.159 4.477,1.918 5.229,3.91l8.7,23.021c0.753,1.992 1.857,1.949 2.455,-0.094z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#f69123">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="84.4913"
|
||||
android:endY="19.211866"
|
||||
android:startX="38.773293"
|
||||
android:startY="80.33152"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFBB22F"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFF58820"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -8,8 +8,11 @@
|
||||
<item
|
||||
android:width="300dp"
|
||||
android:height="300dp"
|
||||
android:drawable="@drawable/ic_logo_color_108dp"
|
||||
android:gravity="center_vertical|center_horizontal"
|
||||
tools:targetApi="m" />
|
||||
android:gravity="center"
|
||||
tools:targetApi="m">
|
||||
<inset
|
||||
android:drawable="@drawable/ic_logo_color_24dp"
|
||||
android:inset="20dp" />
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@@ -13,9 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/chart_view_layout"
|
||||
|
||||
<de.dennisguse.opentracks.chart.ChartView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/chart_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
android:id="@+id/stats_altitude_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit" />
|
||||
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit,@+id/stats_altitude_current_label_egm" />
|
||||
|
||||
<!-- altitude -->
|
||||
<TextView
|
||||
@@ -282,7 +282,7 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_current_label_egm"
|
||||
android:id="@+id/stats_altitude_current_label_egm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -339,11 +339,6 @@ limitations under the License.
|
||||
<string name="value_adapt_accuracy">تكييف من أجل الدقة</string>
|
||||
<string name="value_adapt_battery_life">التكيف بحسب عمر البطارية</string>
|
||||
<string name="value_always">دائمًا</string>
|
||||
<string name="value_float_feet">%1$.2f قدم</string>
|
||||
<string name="value_float_kilometer">%1$.2f كم</string>
|
||||
<string name="value_float_meter">%1$.2f م</string>
|
||||
<string name="value_float_mile">%1$.2f ميل</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f ميل (إشارة نظام تحديد المواقع سيئة)</string>
|
||||
<string name="value_integer_feet">%1$d قدم</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d قدم (إشارة نظام تحديد المواقع ممتازة)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d قدم (موصى به)</string>
|
||||
|
||||
@@ -291,11 +291,6 @@
|
||||
<string name="value_adapt_accuracy">Adapt for accuracy</string>
|
||||
<string name="value_adapt_battery_life">Adapt for battery life</string>
|
||||
<string name="value_always">Always</string>
|
||||
<string name="value_float_feet">%1$.2f ft</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f mi</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f mi (poor GPS)</string>
|
||||
<string name="value_integer_feet">%1$d ft</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d ft (excellent GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d ft (recommended)</string>
|
||||
|
||||
@@ -66,12 +66,12 @@
|
||||
<string name="activity_type_walking">caminata</string>
|
||||
<string name="activity_type_water_skiing">esquí acuático</string>
|
||||
<string name="activity_type_wind_surfing">windsurf</string>
|
||||
<string name="description_activity_type">Tipo de actividad: %1$s.</string>
|
||||
<string name="description_activity_type">Tipo de actividad: %1$s</string>
|
||||
<string name="description_average_moving_pace_in_minute">Velocidad promedio de movimiento: %1$s min/km (%2$s min/mi)</string>
|
||||
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h).</string>
|
||||
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||
<string name="description_average_pace_in_minute">Velocidad promedio: %1$s min/km (%2$s min/mi)</string>
|
||||
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h).</string>
|
||||
<string name="description_altitude_gain">Altitud: %1$d m (%2$d pies).</string>
|
||||
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||
<string name="description_altitude_gain">Variación de altitud: %1$d m (%2$d pies)</string>
|
||||
<string name="description_altitude_imperial">Elevación (pies)</string>
|
||||
<string name="description_altitude_metric">Elevación (m)</string>
|
||||
<string name="description_fastest_pace_in_minute">Velocidad más rápida: %1$s min/km (%2$s min/mi)</string>
|
||||
@@ -81,31 +81,31 @@
|
||||
<string name="description_location_latitude">Latitud (grados)</string>
|
||||
<string name="description_location_longitude">Longitud (grados)</string>
|
||||
<string name="description_location_speed">Velocidad (m/s)</string>
|
||||
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies).</string>
|
||||
<string name="description_max_grade">Pendiente máx.: %1$d %%.</string>
|
||||
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h).</string>
|
||||
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies).</string>
|
||||
<string name="description_min_grade">Pendiente mín.: %1$d %%.</string>
|
||||
<string name="description_moving_time">Tiempo en movimiento: %1$s.</string>
|
||||
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies)</string>
|
||||
<string name="description_max_grade">Pendiente máx.: %1$d %%</string>
|
||||
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies)</string>
|
||||
<string name="description_min_grade">Pendiente mín.: %1$d %%</string>
|
||||
<string name="description_moving_time">Tiempo en movimiento: %1$s</string>
|
||||
<string name="description_pace_imperial">Velocidad (min/mi)</string>
|
||||
<string name="description_pace_metric">Velocidad (min/km)</string>
|
||||
<string name="description_recorded_time">Registrado: %1$s.</string>
|
||||
<string name="description_recorded_time">Registrado: %1$s</string>
|
||||
<string name="description_sensor_cadence">Cadencia (rpm)</string>
|
||||
<string name="description_sensor_heart_rate">Frecuencia cardíaca (lpm)</string>
|
||||
<string name="description_sensor_power">Potencia (W)</string>
|
||||
<string name="description_speed_imperial">Velocidad (mi/h)</string>
|
||||
<string name="description_speed_metric">Velocidad (km/h)</string>
|
||||
<string name="description_time">Tiempo</string>
|
||||
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi).</string>
|
||||
<string name="description_total_time">Tiempo total: %1$s.</string>
|
||||
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi)</string>
|
||||
<string name="description_total_time">Tiempo total: %1$s</string>
|
||||
<string name="description_track_point">Punto</string>
|
||||
<string name="description_track_segment">Segmento</string>
|
||||
<string name="export_all_option">Todo como %1$s</string>
|
||||
<string name="export_all_title">Exportar a unidad de almacenamiento externa</string>
|
||||
<string name="export_error">Se exportaron %1$d de %2$s a %3$s.</string>
|
||||
<string name="export_error">Se exportaron %1$d de %2$s a %3$s</string>
|
||||
<string name="export_option">como %1$s a %2$s</string>
|
||||
<string name="export_progress_message">Exportando a %1$s…</string>
|
||||
<string name="export_success">Se exportaron %1$s a %2$s.</string>
|
||||
<string name="export_success">Se exportaron %1$s a %2$s</string>
|
||||
<string name="export_title">Exportar a…</string>
|
||||
<string name="files_few">%1$d archivos</string>
|
||||
<string name="files_many">%1$d archivos</string>
|
||||
@@ -134,7 +134,7 @@
|
||||
<string name="gps_location_access">“Acceso a ubicaciones”</string>
|
||||
<string name="gps_disabled">El GPS no está habilitado. Puntea aquí para ir a %1$s. Para volver, presiona el botón Atrás.</string>
|
||||
<string name="gps_disabled_my_location">No se puede determinar la ubicación actual. Habilita más fuentes de ubicación en %1$s y asegúrate de que se pueda ver el cielo. Vuelve a intentarlo más tarde.</string>
|
||||
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo.</string>
|
||||
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo</string>
|
||||
<string name="gps_starting">Iniciando GPS</string>
|
||||
<string name="gps_wait_for_better_signal">Esperando una mejor señal de GPS</string>
|
||||
<string name="gps_wait_for_signal">Esperando señal de GPS</string>
|
||||
@@ -148,21 +148,21 @@
|
||||
<string name="image_record">Grabar</string>
|
||||
<string name="image_stop">Parar</string>
|
||||
<string name="image_track">Seguir</string>
|
||||
<string name="import_error">Se importaron %1$d de %2$s desde %3$s.</string>
|
||||
<string name="import_no_directory">%1$s no existe.</string>
|
||||
<string name="import_error">Se importaron %1$d de %2$s desde %3$s</string>
|
||||
<string name="import_no_directory">%1$s no existe</string>
|
||||
<string name="import_no_file">No hay archivos para importar. Conecta la unidad a la computadora y pasa los archivos que deseas importar a %1$s.</string>
|
||||
<string name="import_no_file_title">Importar archivos</string>
|
||||
<string name="import_progress_message">Importando desde %1$s…</string>
|
||||
<string name="import_selection_option">Todos los archivos %1$s</string>
|
||||
<string name="import_selection_title">Importar desde la unidad de almacenamiento externa</string>
|
||||
<string name="import_success">Se importaron %1$s desde %2$s.</string>
|
||||
<string name="import_success">Se importaron %1$s desde %2$s</string>
|
||||
<string name="marker_add_error">No se puede insertar un marcador. No se recibe señal de GPS. Vuelve a intentarlo.</string>
|
||||
<string name="marker_add_success">Se insertó un marcador.</string>
|
||||
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador.</string>
|
||||
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente.</string>
|
||||
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente.</string>
|
||||
<string name="marker_add_success">Se insertó un marcador</string>
|
||||
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador</string>
|
||||
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente</string>
|
||||
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente</string>
|
||||
<string name="marker_delete_one_confirm_title">¿Eliminar marcador?</string>
|
||||
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente.</string>
|
||||
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente</string>
|
||||
<string name="marker_label_end">%1$s (Fin)</string>
|
||||
<string name="marker_label_start">%1$s (Inicio)</string>
|
||||
<string name="marker_name_format">Marcador %1$d</string>
|
||||
@@ -171,7 +171,7 @@
|
||||
<string name="marker_detail_statistics_title">Desde el último marcador de división:</string>
|
||||
<string name="marker_edit_marker_type">Tipo de marcador (p. ej., cascada)</string>
|
||||
<string name="marker_edit_marker_type_hint">Tipo de marcador</string>
|
||||
<string name="marker_list_empty_message">No tienes ningún marcador.</string>
|
||||
<string name="marker_list_empty_message">No tienes ningún marcador</string>
|
||||
<string name="menu_aggregated_statistics">Estadíst. globales</string>
|
||||
<string name="menu_delete">Eliminar</string>
|
||||
<string name="menu_edit">Editar</string>
|
||||
@@ -196,17 +196,17 @@
|
||||
<string name="menu_resume_track">Reanudar ruta</string>
|
||||
<string name="recording_service">Servicio de grabación</string>
|
||||
<string name="search_global_description">Recorridos y marcadores</string>
|
||||
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda.</string>
|
||||
<string name="search_list_marker_track_location">En el recorrido “%1$s”</string>
|
||||
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda</string>
|
||||
<string name="search_list_marker_track_location">En el recorrido “%1$s”.</string>
|
||||
<string name="sensor_state_battery">Nivel de la batería</string>
|
||||
<string name="sensor_state_cadence">Cadencia</string>
|
||||
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
||||
<string name="sensor_state_disconnected">Desconectado</string>
|
||||
<string name="sensor_state_heart_rate">Frecuencia cardíaca</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d lpm</string>
|
||||
<string name="sensor_state_power">Potencia</string>
|
||||
<string name="sensor_state_power_value">%1$d W</string>
|
||||
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido.</string>
|
||||
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido</string>
|
||||
<string name="settings_show">Mostrar</string>
|
||||
<string name="settings_advanced">Opciones avanzadas</string>
|
||||
<string name="settings_chart_by_distance">Por distancia</string>
|
||||
@@ -216,23 +216,23 @@
|
||||
<string name="settings_recording_auto_resume_track_timeout_always_summary">Siempre reanudar la grabación después de reiniciar</string>
|
||||
<string name="settings_recording_auto_resume_track_timeout_never_summary">Nunca reanudar la grabación después de reiniciar</string>
|
||||
<string name="settings_recording_auto_resume_track_timeout_summary">Reanudar una grabación al reiniciar el sistema si la última ubicación se grabó en los últimos %1$s</string>
|
||||
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación autom.</string>
|
||||
<string name="settings_recording_default_activity_title">Actividad predet.</string>
|
||||
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación automática</string>
|
||||
<string name="settings_recording_default_activity_title">Tipo de actividad predeterminada</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s entre las ubicaciones grabadas</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s.</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Distancia máxima</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Intervalo de distancia</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Intervalo de tiempo</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s.</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">Precisión del GPS</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Fecha (ISO 8601)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Fecha (local)</string>
|
||||
<string name="settings_recording_track_name_number_option">Número</string>
|
||||
<string name="settings_recording_track_name_title">Nombre de rec. predet.</string>
|
||||
<string name="settings_recording_track_name_title">Nombre de recorrido predeterminado</string>
|
||||
<string name="settings_reset">Restablecer</string>
|
||||
<string name="settings_reset_confirm_message">Todos los parámetros de configuración se restablecerán a los valores predeterminados.</string>
|
||||
<string name="settings_reset_confirm_title">¿Restablecer ahora?</string>
|
||||
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración.</string>
|
||||
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración</string>
|
||||
<string name="settings_sensor">Sensor</string>
|
||||
<string name="settings_sensor_bluetooth">Bluetooth</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||
@@ -242,7 +242,9 @@
|
||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||
<string name="settings_stats_units_title">Unidades preferidas</string>
|
||||
<string name="share_track_share_file">Compartir el archivo del recorrido</string>
|
||||
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.\n\nPara ver este recorrido, usa Google Earth u otro software de mapas.\n\n%1$s</string>
|
||||
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.
|
||||
\n
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">Me gustaría compartir un recorrido contigo</string>
|
||||
<string name="stats_average_moving_pace">Ritmo de movimiento promedio</string>
|
||||
<string name="stats_average_moving_speed">Velocidad de movimiento promedio</string>
|
||||
@@ -255,7 +257,7 @@
|
||||
<string name="stats_latitude">Latitud</string>
|
||||
<string name="stats_longitude">Longitud</string>
|
||||
<string name="stats_max">Máx.</string>
|
||||
<string name="stats_max_speed">Velocidad máx.</string>
|
||||
<string name="stats_max_speed">Velocidad máxima</string>
|
||||
<string name="stats_min">Mín.</string>
|
||||
<string name="stats_moving_time">Tiempo en movimiento</string>
|
||||
<string name="stats_pace">Ritmo</string>
|
||||
@@ -275,7 +277,7 @@
|
||||
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo)</string>
|
||||
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
|
||||
<string name="track_edit_new_track_title">Nuevo recorrido</string>
|
||||
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura.</string>
|
||||
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura</string>
|
||||
<string name="tracks_few">%1$d recorridos</string>
|
||||
<string name="tracks_many">%1$d recorridos</string>
|
||||
<string name="tracks_one">1 recorrido</string>
|
||||
@@ -293,11 +295,6 @@
|
||||
<string name="value_adapt_accuracy">Adaptar para precisión</string>
|
||||
<string name="value_adapt_battery_life">Adaptar a la duración de la batería</string>
|
||||
<string name="value_always">Siempre</string>
|
||||
<string name="value_float_feet">%1$.2f pies</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f millas</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f millas (señal de GPS deficiente)</string>
|
||||
<string name="value_integer_feet">%1$d pies</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d pies (señal de GPS excelente)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d pies (recomendado)</string>
|
||||
@@ -309,7 +306,7 @@
|
||||
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
|
||||
<string name="value_integer_mile">%1$d millas</string>
|
||||
<string name="value_integer_mile_hour">%1$d mi/h</string>
|
||||
<string name="value_integer_minute">%1$d min.</string>
|
||||
<string name="value_integer_minute">%1$d mín</string>
|
||||
<string name="value_integer_percent">%1$d %%</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
<string name="value_none">Ninguno</string>
|
||||
@@ -416,4 +413,141 @@
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
|
||||
<string name="share_image_body">Creo que esta imagen podría interesarte.</string>
|
||||
</resources>
|
||||
<string name="about_preference_title">Acerca de OpenTracks</string>
|
||||
<string name="about_description"><i> OpenTracks </i> es una aplicación de seguimiento deportivo que respeta completamente su privacidad.</string>
|
||||
<string name="about_url">Página web del proyecto: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Nombre de la versión: %1$s</string>
|
||||
<string name="bluetooth_disabled">Por favor habilite Bluetooth.</string>
|
||||
<string name="settings_default_trackfileformat">Exportar/compartir formato de archivo</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Mostrar estadísticas en pantalla de bloqueo</string>
|
||||
<string name="permission_gps_failed">OpenTracks necesita permiso para usar el GPS.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Mantener la pantalla encendida</string>
|
||||
<string name="settings_sensor_disconnected">Desconectado de %1$s</string>
|
||||
<string name="instant_export_enabled_title">Exportación instantánea post-entrenamiento</string>
|
||||
<string name="hold_to_stop">Mantener para parar</string>
|
||||
<string name="hold_to_pause">Mantener para pausar</string>
|
||||
<string name="settings_sensor_wheel_circumference">Circunferencia de la rueda (mm)</string>
|
||||
<string name="sensor_state_cadence_avg">Ritmo promedio</string>
|
||||
<string name="sensor_state_heart_rate_avg">Tasa cardíaca promedio</string>
|
||||
<string name="sensor_state_heart_rate_max">Tasa cardíaca máxima</string>
|
||||
<string name="sensor_state_cadence_max">Ritmo máximo</string>
|
||||
<string name="stats_intervals">Intervalos</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Mientras graba, mostrar estadísticas sin desbloquear el dispositivo.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Mientras graba, mantener la pantalla encendida.</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_title">Pantalla completa</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Mientras graba, presentar en pantalla completa.</string>
|
||||
<string name="settings_night_mode_title">Tema de interfaz de usuario</string>
|
||||
<string name="settings_night_mode_option_system">Sistema</string>
|
||||
<string name="settings_night_mode_option_day">Día</string>
|
||||
<string name="settings_night_mode_option_night">Noche</string>
|
||||
<string name="settings_error_initial_values_restored">Lo sentimos, pero ha ocurrido un error. Todas las configuraciones se han revertido a los valores predeterminados.</string>
|
||||
<string name="settings_sensor_connecting">Conectado a %1$s</string>
|
||||
<string name="track_speed_notification">Velocidad: %1$s</string>
|
||||
<string name="instant_export_enabled_summary">Exportar el recorrido al almacenamiento una vez finalizada la grabación</string>
|
||||
<string name="toast_message_instant_track_was_exported">Último recorrido exportado</string>
|
||||
<string name="description_altitude_loss">Pérdida de elevación: %1$d m (%2$d pies)</string>
|
||||
<string name="export_progress_summary_new_msg">Nuevos recorridos exportados</string>
|
||||
<string name="about_version_code">Código de versión: %1$d</string>
|
||||
<string name="about_history">OpenTracks es una bifurcación de <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks de Google </i> </a> (descontinuado en 2016).</string>
|
||||
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de sensor de frecuencia cardíaca, esto significa que solo se almacena <i> una </i> medición de frecuencia cardíaca por ubicación. Si está parado o está adentro, no se registrarán los datos del sensor (pero se mostrarán). </string>
|
||||
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer:
|
||||
\na) compruebe si el GPS está habilitado,
|
||||
\nb) mejorar la recepción del GPS (por ejemplo, salir al aire libre),
|
||||
\nc) verifique con la aplicación <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a>, y
|
||||
\nd) desactivar el ahorro de energía.
|
||||
\n
|
||||
\nSi todo no ayuda, informe un error. </string>
|
||||
<string name="help_support_content"> Usted puede:
|
||||
\na) haga correr la voz sobre OpenTracks,
|
||||
\nb) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> proponer funciones </a>,
|
||||
\nc) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> informar errores </a>,
|
||||
\nd) ayuda en el desarrollo de software,
|
||||
\ne) comprar en <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
|
||||
\nf) donar, y
|
||||
\ng) haga lo que crea que ayuda a OpenTracks. </string>
|
||||
<string name="export_error_post_workout">La exportación posterior al entrenamiento falló, consulte el directorio de exportación.</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">Completado con un error</item>
|
||||
<item quantity="other">Completado con %1$d errores</item>
|
||||
</plurals>
|
||||
<string name="import_thread_interrupted">Hilo interrumpido</string>
|
||||
<string name="import_no_kml_file_found">No se ha encontrado ningún archivo doc.kml en KMZ</string>
|
||||
<string name="sensor_state_power_avg">Potencia promedio</string>
|
||||
<string name="sensor_not_known">El sensor %1$s es desconocido. Compruebe la configuración.</string>
|
||||
<string name="sensor_could_not_scan">No se pudieron buscar dispositivos Bluetooth (error %1$i).</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Evitar reimportar los recorridos</string>
|
||||
<string name="track_distance_notification">Distancia: %1$s</string>
|
||||
<string name="aggregated_stats_empty_message">Grabe su primer recorrido para ver estadísticas agregadas</string>
|
||||
<string name="settings_default_export_uri_title">Directorio de exportación de recorrido</string>
|
||||
<string name="help_track_map_content"> Utilice el icono de mapa pequeño.
|
||||
\na) (actualizaciones en vivo) Instale <a href="https://github.com/OpenTracksApp/OSMDashboard"> OSMDashboard </a>. Proporciona mapas en línea o fuera de línea a través de <a href="https://www.openstreetmap.org"> OpenStreetMap </a>.
|
||||
\nEn realidad, OSMDashboard es una aplicación separada que solo puede acceder (temporalmente) a los datos de las pistas que se mostrarán.
|
||||
\nb) A través de una aplicación de mapas normal (p. ej., <a href="http://osmand.net/"> OsmAnd </a> y <a href="https://maps.me/"> MAPS.ME </a>). Estas aplicaciones deben admitir el formato de archivo KMZ (solo ubicaciones y marcas de tiempo). De hecho, una aplicación debe admitir <i> ACTION_VIEW </i> con MIME <i> application / vnd.google-earth.kmz</i>. </string>
|
||||
<string name="voice_speed_lap">Velocidad de vuelta de %1$s</string>
|
||||
<string name="voice_pace_lap">Tiempo de vuelta de %1$s</string>
|
||||
<string name="delete_service">Borrar servicio</string>
|
||||
<string name="about_third_party_data">Datos de terceros</string>
|
||||
<string name="about_libraries">Bibliotecas</string>
|
||||
<string name="help_feature_title">OpenTracks tiene las siguientes características:</string>
|
||||
<string name="help_feature_content">* crear pistas de GPS
|
||||
\n* utilizar un sensor de frecuencia cardíaca (solo Bluetooth LE)
|
||||
\n* anuncios de voz (por tiempo o distancia)
|
||||
\n* crear marcadores (incluidas imágenes)
|
||||
\n* funciona completamente sin conexión
|
||||
\n* mostrar pistas en un mapa (ver más abajo)</string>
|
||||
<string name="help_track_map_title">¿Cómo mostrar un camino en un mapa\?</string>
|
||||
<string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string>
|
||||
<string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string>
|
||||
<string name="help_track_export_title">¿Cómo puedo exportar / compartir pistas grabadas\?</string>
|
||||
<string name="help_track_export_content"> Las pistas se pueden exportar desde la configuración como KMZ, KML o GPX. KMZ / KML todos los datos; para GPX solo ubicaciones. Las pistas compartidas a través del <i> icono de compartir </i> se comparten como KMZ. </string>
|
||||
<string name="help_data_google_play_services_title">¿OpenTracks requiere / usa el servicio Google Play\?</string>
|
||||
<string name="help_recording_title">¿Cómo registra OpenTracks los datos\?</string>
|
||||
<string name="help_recording_content">OpenTracks utiliza el sensor GPS del dispositivo para registrar la ubicación de forma continua. Si el dispositivo GPS informa datos inexactos (por ejemplo, ubicación, velocidad, elevación), OpenTracks no puede hacer nada al respecto.</string>
|
||||
<string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores\?</string>
|
||||
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
|
||||
<string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string>
|
||||
<string name="open_track_using_dashboard">Abrir usando el Tablero OpenTracks</string>
|
||||
<string name="open_track_as_KMZ">Abrir como KMZ</string>
|
||||
<string name="open_track_as_GPX">Abrir como GPX</string>
|
||||
<string name="open_track_as_trackfileformat">Abrir como %1$s</string>
|
||||
<string name="activity_type_escooter">moto</string>
|
||||
<string name="app_not_installed_show_on_map">Instale una aplicación que admita la presentación de datos de mapas.</string>
|
||||
<string name="description_speed_ms">Velocidad (m/s)</string>
|
||||
<string name="description_default_speed_or_pace">Por tipo de actividad</string>
|
||||
<string name="export_with_photos">con fotos</string>
|
||||
<string name="export_without_photos">sin fotos</string>
|
||||
<string name="export_track_already_exists_msg">La pista %1$s ya existe en el directorio de destino.</string>
|
||||
<string name="export_all_do_it_for_all">hacerlo para todos los conflictos</string>
|
||||
<string name="export_track_errors">Recorridos no exportados</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Recorridos sobreescritos</string>
|
||||
<string name="export_progress_summary_skip_msg">Recorridos salteados</string>
|
||||
<string name="export_progress_summary_errors_msg">Errores</string>
|
||||
<string name="generic_open_track">Abrir recorrido</string>
|
||||
<string name="generic_overwrite">Sobreescribir</string>
|
||||
<string name="generic_skip">Saltear</string>
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="generic_show_errors">Mostrar errores</string>
|
||||
<string name="generic_completed">Completado</string>
|
||||
<string name="generic_click_twice_cancel">Haga nuevamente clic en atrás para cancelar</string>
|
||||
<string name="gps_disabled_msg">GPS deshabilitado</string>
|
||||
<string name="gps_fixed_and_ready">GPS establecido y listo</string>
|
||||
<string name="import_unsupported_format">Formato de archivo no soportado</string>
|
||||
<string name="import_parser_error">Error de parseo: %1$s</string>
|
||||
<string name="import_unable_to_import_file">No se puede importar el archivo: %1$s</string>
|
||||
<string name="import_error_info">%1$s: %2$s</string>
|
||||
<string name="import_error_list_dialog_title">Archivos no importados</string>
|
||||
<string name="import_file_imported">Archivo %1$s importado</string>
|
||||
<string name="import_prevent_reimport">Ha configurado evitar la reimportación del recorrido</string>
|
||||
<string name="import_progress_summary_ok_msg">Archivos importados</string>
|
||||
<string name="import_progress_summary_exists_msg">Ya existe</string>
|
||||
<string name="import_progress_summary_errors_msg">Errores</string>
|
||||
<string name="marker_add_photo_canceled">Cancelado, no se agregó la foto como marcador</string>
|
||||
<string name="stats_loss">Pérdida</string>
|
||||
<string name="stats_split_interval">Intervalo partido</string>
|
||||
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="track_detail_intervals_tab">Intervalos</string>
|
||||
<string name="track_detail_resume_success">Ahora reanudando el recorrido</string>
|
||||
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
|
||||
</resources>
|
||||
@@ -341,11 +341,6 @@ limitations under the License.
|
||||
<string name="value_adapt_accuracy">Адаптиране за точност</string>
|
||||
<string name="value_adapt_battery_life">Адаптиране спрямо батерията</string>
|
||||
<string name="value_always">Винаги</string>
|
||||
<string name="value_float_feet">%1$.2f фт</string>
|
||||
<string name="value_float_kilometer">%1$.2f км</string>
|
||||
<string name="value_float_meter">%1$.2f м</string>
|
||||
<string name="value_float_mile">%1$.2f мил</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f мил (слаб GPS)</string>
|
||||
<string name="value_integer_feet">%1$d фт</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d фт (отличен GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d фт (препоръчително)</string>
|
||||
|
||||
@@ -341,11 +341,6 @@ es vegi el cel. Torneu-ho a provar més tard.</string>
|
||||
<string name="value_adapt_accuracy">Adapta\'n la precisió</string>
|
||||
<string name="value_adapt_battery_life">Adapta a la durada de la bateria</string>
|
||||
<string name="value_always">Sempre</string>
|
||||
<string name="value_float_feet">%1$.2f peus</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f milles</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f milles (GPS amb poca senyal)</string>
|
||||
<string name="value_integer_feet">%1$d peus</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d peus (senyal de GPS excel·lent)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d peus (recomanat)</string>
|
||||
|
||||
@@ -411,11 +411,6 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
|
||||
<string name="value_adapt_accuracy">Upřednostnit přesnost</string>
|
||||
<string name="value_adapt_battery_life">Upřednostnit výdrži baterie</string>
|
||||
<string name="value_always">Vždy</string>
|
||||
<string name="value_float_feet">%1$.2f stop</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f mil</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f mil (špatný signál GPS)</string>
|
||||
<string name="value_integer_feet">%1$d stop</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d stop (vynikající signál GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d stop (doporučeno)</string>
|
||||
@@ -556,10 +551,6 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
|
||||
<string name="stats_intervals">Intervaly</string>
|
||||
<string name="stats_split_interval">Rozdělit interval</string>
|
||||
<string name="track_detail_intervals_tab">Intervaly</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mil/h</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="voice_pace_lap">Čas na kolo %1$s</string>
|
||||
<string name="hold_to_pause">Podržet pro pozastavení</string>
|
||||
<string name="toast_message_instant_track_was_exported">Poslední trasa exportována</string>
|
||||
@@ -612,4 +603,6 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
|
||||
<string name="track_speed_notification">Rychlost: %1$s</string>
|
||||
<string name="track_pace_notification">Rychlost: %1$s</string>
|
||||
<string name="about_third_party_data">Údaje třetích stran</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="delete_service">Služba mazání</string>
|
||||
</resources>
|
||||
@@ -417,11 +417,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="value_adapt_accuracy">Juster nøjagtighed</string>
|
||||
<string name="value_adapt_battery_life">Tilpas til batterilevetid</string>
|
||||
<string name="value_always">Altid</string>
|
||||
<string name="value_float_feet">%1$.2f fod</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f mil</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f mil (dårlig GPS)</string>
|
||||
<string name="value_integer_feet">%1$d fod</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d fod (fremragende GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d fod (anbefales)</string>
|
||||
@@ -545,10 +540,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="open_track_as_trackfileformat">Åbn som %1$s</string>
|
||||
<string name="activity_type_escooter">elektrisk scooter</string>
|
||||
<string name="generic_click_twice_cancel">Klik tilbage igen for at annullere</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f km/t</string>
|
||||
<string name="import_unable_to_import_file">Kunne ikke importere filen:%1$s</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="import_prevent_reimport">Du har indstillet forhindre genimport af spor</string>
|
||||
<string name="settings_default_export_uri_title">Spor eksportmappe</string>
|
||||
<string name="instant_export_enabled_summary">Eksporter spor til lagring, når optagelsen er færdig</string>
|
||||
@@ -583,7 +575,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="interval_list_empty_message">Der er ikke noget interval endnu</string>
|
||||
<string name="aggregated_stats_empty_message">Optag dit første spor for at se aggregerede statistikker</string>
|
||||
<string name="track_detail_intervals_tab">Intervaller</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mph</string>
|
||||
<string name="voice_pace_lap">Omgangstid på %1$s</string>
|
||||
<string name="voice_speed_lap">Omgangshastighed på %1$s</string>
|
||||
<string name="hold_to_pause">Hold nede for at holde pause</string>
|
||||
@@ -614,4 +605,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="track_distance_notification">Afstand: %1$s</string>
|
||||
<string name="track_speed_notification">Hastighed: %1$s</string>
|
||||
<string name="about_third_party_data">Data fra tredjeparter</string>
|
||||
<string name="delete_service">Tjeneste til sletning</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
</resources>
|
||||
@@ -360,7 +360,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
|
||||
<string name="share_track_subject">Ich möchte einen Track mit dir teilen</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">Durchschnitt\n(Bewegung)</string>
|
||||
<string name="stats_average_moving_speed">Durchschnittsgeshwindigkeit
|
||||
<string name="stats_average_moving_speed">Durchschnittsgeschwindigkeit
|
||||
\n(Bewegung)</string>
|
||||
<string name="stats_average_pace">Durchschnitt</string>
|
||||
<string name="stats_average_speed">Durchschnitt</string>
|
||||
@@ -422,11 +422,6 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
|
||||
<string name="value_adapt_accuracy">Genauigkeit anpassen</string>
|
||||
<string name="value_adapt_battery_life">An Akkulaufzeit anpassen</string>
|
||||
<string name="value_always">Immer</string>
|
||||
<string name="value_float_feet">%1$.2f ft</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f Meilen</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f Meilen (schlechtes GPS-Signal)</string>
|
||||
<string name="value_integer_feet">%1$d Fuß</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d Fuß (ausgezeichnetes GPS-Signal)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d Fuß (empfohlen)</string>
|
||||
@@ -601,6 +596,4 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
|
||||
<string name="track_distance_notification">Strecke: %1$s</string>
|
||||
<string name="track_speed_notification">Geschwindigkeit: %1$s</string>
|
||||
<string name="track_pace_notification">Tempo: %1$s</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -340,11 +340,6 @@ limitations under the License.
|
||||
<string name="value_adapt_accuracy">Προσαρμογή για ακρίβεια</string>
|
||||
<string name="value_adapt_battery_life">Καλύτερο για μπατ.</string>
|
||||
<string name="value_always">Πάντα</string>
|
||||
<string name="value_float_feet">%1$.2f πόδια</string>
|
||||
<string name="value_float_kilometer">%1$.2f χιλιόμετρα</string>
|
||||
<string name="value_float_meter">%1$.2f μέτρα</string>
|
||||
<string name="value_float_mile">%1$.2f μίλια</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f μίλια (χαμηλό σήμα GPS)</string>
|
||||
<string name="value_integer_feet">%1$d πόδια</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d πόδια (τέλειο σήμα GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d ft (συνιστάται)</string>
|
||||
|
||||
@@ -412,11 +412,6 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
|
||||
<string name="value_adapt_accuracy">Adaptar para obtener valores exactos</string>
|
||||
<string name="value_adapt_battery_life">Adaptar a la duración de la batería</string>
|
||||
<string name="value_always">Siempre</string>
|
||||
<string name="value_float_feet">%1$.2f pies</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f millas</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f millas (señal de GPS débil)</string>
|
||||
<string name="value_integer_feet">%1$d pies</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d pies (señal de GPS excelente)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d pies (recomendado)</string>
|
||||
@@ -595,12 +590,8 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
|
||||
<string name="import_progress_summary_errors_msg">Errores</string>
|
||||
<string name="import_progress_summary_ok_msg">Archivos importados</string>
|
||||
<string name="description_altitude_loss">Pérdida de altura: %1$d m (%2$d pies)</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mph</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="sensor_state_cadence_max">Cadencia máxima</string>
|
||||
<string name="sensor_state_cadence_avg">Cadencia media</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Pistas sobrescritas</string>
|
||||
<string name="export_progress_summary_skip_msg">Pistas omitidas</string>
|
||||
<string name="export_progress_summary_errors_msg">Errores</string>
|
||||
@@ -612,4 +603,6 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
|
||||
<string name="track_speed_notification">Velocidad: %1$s</string>
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="about_third_party_data">Datos de terceros</string>
|
||||
<string name="delete_service">Eliminar servicio</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
</resources>
|
||||
@@ -411,11 +411,6 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
|
||||
<string name="value_adapt_accuracy">Täpsuse kohandamine</string>
|
||||
<string name="value_adapt_battery_life">Kohandage aku elueale</string>
|
||||
<string name="value_always">Alati</string>
|
||||
<string name="value_float_feet">%1$.2f jalga</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f miili</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f miili (nõrk GPS-signaal)</string>
|
||||
<string name="value_integer_feet">%1$d jalga</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d jalga (tugev GPS-signaal)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d jalga (soovitatav)</string>
|
||||
@@ -579,10 +574,6 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
|
||||
<string name="stats_split_interval">Jagatud intervall</string>
|
||||
<string name="stats_intervals">Intervallid</string>
|
||||
<string name="interval_list_empty_message">Ei ole ühtegi intervalli veel</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mph</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
|
||||
<string name="voice_speed_lap">Ringi kiirus %1$s</string>
|
||||
<string name="hold_to_stop">Hoidke lõpetada</string>
|
||||
<string name="hold_to_pause">Hoidke pausi</string>
|
||||
@@ -611,4 +602,6 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
|
||||
<string name="track_speed_notification">Kiirus: %1$s</string>
|
||||
<string name="track_pace_notification">Tempo: %1$s</string>
|
||||
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
|
||||
<string name="delete_service">Kustutamise teenus</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
</resources>
|
||||
@@ -412,11 +412,6 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
|
||||
<string name="value_adapt_accuracy">Doitu zehaztasuna</string>
|
||||
<string name="value_adapt_battery_life">Doitu bateriaren iraupenera</string>
|
||||
<string name="value_always">Beti</string>
|
||||
<string name="value_float_feet">%1$.2f oin</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f mi</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f mi (GPS ahula)</string>
|
||||
<string name="value_integer_feet">%1$d ft</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d ft (GPS bikaina)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d oin (gomendatua)</string>
|
||||
@@ -543,13 +538,9 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
|
||||
<string name="activity_type_escooter">eskooterra</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Grabatzen ari zaren bitartean, pantaila osoan ikusi.</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_title">Pantaila osoa</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mph</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||
<string name="settings_default_export_uri_title">Track esportazio-direktorioa</string>
|
||||
<string name="instant_export_enabled_title">Berehalako post-entrenamendu esportatu</string>
|
||||
<string name="instant_export_enabled_summary">Esportazio-pista biltegiratze ondoren grabaketa amaitu da</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="interval_list_empty_message">Ez dira edozein tarte oraindik</string>
|
||||
<string name="aggregated_stats_empty_message">Grabatu zure lehen pista ikusteko batzen estatistikak</string>
|
||||
<string name="open_track_as_GPX">Ireki gisa GPX</string>
|
||||
@@ -612,4 +603,6 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
|
||||
<string name="track_speed_notification">Abiadura: %1$s</string>
|
||||
<string name="track_pace_notification">Erritmoa: %1$s</string>
|
||||
<string name="about_third_party_data">Hirugarrenen datuak</string>
|
||||
<string name="delete_service">Ezabatzeko zerbitzua</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
</resources>
|
||||
@@ -339,11 +339,6 @@ limitations under the License.
|
||||
<string name="value_adapt_accuracy">تعدیل به خاطر دقت</string>
|
||||
<string name="value_adapt_battery_life">تعدیل به خاطر عمر باتری</string>
|
||||
<string name="value_always">همیشه</string>
|
||||
<string name="value_float_feet">%1$.2f فوت</string>
|
||||
<string name="value_float_kilometer">%1$.2f کیلومتر</string>
|
||||
<string name="value_float_meter">%1$.2f متر</string>
|
||||
<string name="value_float_mile">%1$.2f مایل</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f مایل (GPS ضعیف)</string>
|
||||
<string name="value_integer_feet">%1$d فوت</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d فوت (GPS عالی)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d فوت (توصیه میشود)</string>
|
||||
|
||||
@@ -422,11 +422,6 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="value_adapt_accuracy">Säädä tarkkuutta</string>
|
||||
<string name="value_adapt_battery_life">Sovita akun kestoon</string>
|
||||
<string name="value_always">Aina</string>
|
||||
<string name="value_float_feet">%1$.2f jalkaa</string>
|
||||
<string name="value_float_kilometer">%1$.2f km</string>
|
||||
<string name="value_float_meter">%1$.2f m</string>
|
||||
<string name="value_float_mile">%1$.2f mailia</string>
|
||||
<string name="value_float_mile_poor_gps">%1$.1f mailia (heikko GPS)</string>
|
||||
<string name="value_integer_feet">%1$d jalkaa</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d jalkaa (erinomainen GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d jalkaa (suositus)</string>
|
||||
@@ -560,7 +555,6 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="settings_default_export_uri_title">Seuraa vientihakemistoa</string>
|
||||
<string name="instant_export_enabled_summary">Vie raita varastoon tallennuksen jälkeen on valmis</string>
|
||||
<string name="voice_speed_lap">Kierrosnopeus %1$s</string>
|
||||
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||
<string name="settings_sensor_wheel_circumference">Pyörän ympärysmitta (mm)</string>
|
||||
<string name="stats_intervals">Välein</string>
|
||||
<string name="import_thread_interrupted">Lanka keskeytetty</string>
|
||||
@@ -605,15 +599,14 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="description_altitude_loss">Korkeussijaintitappio: %1$d m (%2$d ft)</string>
|
||||
<string name="sensor_state_heart_rate_max">Maksimisyke</string>
|
||||
<string name="sensor_state_heart_rate_avg">Avg syke</string>
|
||||
<string name="value_float_kilometer_hour">%1$.2f k/t</string>
|
||||
<string name="value_pace_mile">%1$d:%2$02d min/mailia</string>
|
||||
<string name="activity_type_escooter">eskootteri</string>
|
||||
<string name="sensor_state_cadence_avg">Keskim.Poljinnopeus</string>
|
||||
<string name="sensor_state_cadence_max">Suurin Poljinnopeus</string>
|
||||
<string name="value_float_mile_hour">%1$.2f mt</string>
|
||||
<string name="generic_open_track">Avattu reitti</string>
|
||||
<string name="track_distance_notification">Etäisyys: %1$s</string>
|
||||
<string name="track_pace_notification">Vauhti: %1$s</string>
|
||||
<string name="track_speed_notification">Nopeus: %1$s</string>
|
||||
<string name="about_third_party_data">Kolmannen osapuolen tiedot</string>
|
||||
<string name="delete_service">Poistopalvelu</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
</resources>
|
||||