forked from upstream-mirrors/OpenTracks
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 117281469d | |||
| 3782a22684 | |||
| aed2025a5a | |||
| 780be5d1a9 | |||
| a2d9b10381 | |||
| 5950c73785 | |||
| 99a9248b27 | |||
| 6bcf1c0818 | |||
| 96dd057287 | |||
| 84aeebea4a | |||
| cfb3ff9985 | |||
| 389c4bed96 | |||
| d8acf7df22 | |||
| 53ea905713 | |||
| 8cceadd93e | |||
| d8eed2ccc9 | |||
| f5190aef06 | |||
| 9834c60f1c | |||
| 6eeb9a0eb2 | |||
| ea23e25a47 | |||
| 53e97a5e4c | |||
| f1f9153ec6 | |||
| e442558eed | |||
| 382acadc6a | |||
| 1ed807054b | |||
| c96635c46b | |||
| 9b2c93f74f | |||
| e7a86b6aad | |||
| b86f8c9a20 | |||
| e9130f3402 | |||
| a08ec97c29 | |||
| a6340d13fa | |||
| dbbf6abcd6 | |||
| 683bf2d280 | |||
| 6c2377a8e8 | |||
| b6661ccafd | |||
| 932c142691 | |||
| 57918249bc | |||
| 3405eca556 | |||
| a932adde3f | |||
| a03dfd81ac | |||
| 7550525462 | |||
| 5d70381b23 | |||
| 96a4cbf70d | |||
| 959aadbb62 | |||
| fa59752c04 | |||
| 5305ec3206 | |||
| 584339b7eb | |||
| 151c3a79de |
@@ -18,7 +18,7 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.30.1
|
||||
uses: reactivecircus/android-emulator-runner@v2.32.0
|
||||
with:
|
||||
arch: x86_64
|
||||
api-level: 34
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
|
||||
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
|
||||
* Garmin HRM-Dual (Reference: 010-12883-00)
|
||||
* CYCPLUS H1
|
||||
|
||||
## 0x1816: Cycling Cadence and Speed Service
|
||||
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.4.1'
|
||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 5788
|
||||
versionName "v4.12.4"
|
||||
versionCode 5837
|
||||
versionName "v4.13.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -127,7 +127,7 @@ android {
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
@@ -137,12 +137,12 @@ dependencies {
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.mediarouter:mediarouter:1.7.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.12.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Změny:
|
||||
- Oznámení: jednotky lze vynechat
|
||||
- Oznámení: přidán výkon pro jízdu na kole
|
||||
|
||||
Oprava chyby:
|
||||
- Pád při odstranění značky ze seznamu
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- Pohyb je nyní opět počítán správně.
|
||||
Byl rozbitý od verze 4.9.0 z důvodu představení funkce přestávky. Viz #1756.
|
||||
|
||||
Změny:
|
||||
- Podpora prediktivního zadávání
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Moving is now computed correctly again.
|
||||
Was broken since v4.9.0 due to the introduction of idle functionality. See #1756.
|
||||
|
||||
Changes:
|
||||
- Support for Predictive Back
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.13.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Markers can be created via Public API
|
||||
This allows to create Markers at any location and after recording.
|
||||
Drawback: Markers are not shown in diagrams anymore.
|
||||
NOTE: OSMDashboard supports this API after version: v4.1.0
|
||||
|
||||
Bugfix:
|
||||
- Default activity setting should have been a dropdown
|
||||
- Back arrow not working in settings
|
||||
- Label for off trail hiking now shows "off trail hiking"
|
||||
|
||||
Developer:
|
||||
- Use Position instead of Android's Location
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- Aankondigingen: eenheden kunnen worden weggelaten
|
||||
- Aankondigingen: extra wielrenkracht
|
||||
|
||||
Bugfix:
|
||||
- Markeringen verwijderen uit lijst gecrasht
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Foutcorrectie:
|
||||
- Verplaatsing wordt weer correct berekend.
|
||||
Was defect sinds v4.9.0 t.g.v. de introductie van inactieve functionaliteit. Zie # 1756.
|
||||
|
||||
Veranderingen:
|
||||
- Ondersteuning voor Predictive Back
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.11.1: OpenTracks
|
||||
|
||||
Hata Düzeltme:
|
||||
- PublicAPI durdurulurken çöküyor
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.11.2: OpenTracks
|
||||
|
||||
Hata Düzeltme:
|
||||
- Ekran kapandığında kayıt durduruluyor (v4.11.1 versiyonundan beri)
|
||||
(geri dönülüyor da3dd29b8e44624a8222caea9067955f252ef5f8)
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.12.1: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Android 14: eksik izinleri bildir (hata yerine)
|
||||
- POST_NOTIFICATION opsiyonel
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.12.2.: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Sensörleri kayıttan önce başlatmaya izin ver
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Değişiklikler:
|
||||
- Duyurular: birimler ihmal edilebilir
|
||||
- Duyurular: tur bisikleti gücü eklendi
|
||||
|
||||
Hata Ayıklama:
|
||||
- İşaretçileri çöken listesinden sil
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Hareketler artık yeniden doğru bir şekilde hesaplanıyor.
|
||||
boşta durulan vakit özelliğinin getirilmesinden dolayı v4.9.0'den beri bozuktu. #1756'ya bakınız
|
||||
|
||||
Değişiklikler:
|
||||
- Tahmine Dayalı Geri Dönüş Desteği eklendi
|
||||
@@ -0,0 +1,10 @@
|
||||
(versiyonAdı): OpenTracks
|
||||
|
||||
Değişiklikler:
|
||||
-
|
||||
|
||||
Hata Ayıklama:
|
||||
-
|
||||
|
||||
Geliştirici:
|
||||
-
|
||||
@@ -0,0 +1 @@
|
||||
OpenTracks
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -88,9 +88,9 @@ public class TestDataUtil {
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
List<Marker> markers = List.of(
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, stats, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, stats, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, stats, trackPoints.get(5), null)
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, trackPoints.get(5), null)
|
||||
);
|
||||
|
||||
return new TrackData(track, trackPoints, markers);
|
||||
@@ -152,7 +152,7 @@ public class TestDataUtil {
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, stats, trackPoint, photoUrl);
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, trackPoint, photoUrl);
|
||||
}
|
||||
|
||||
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
|
||||
|
||||
+4
-8
@@ -9,7 +9,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
@@ -65,7 +64,6 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
@@ -141,7 +139,7 @@ public class ExportImportTest {
|
||||
Distance sensorDistance = Distance.of(10); // recording distance interval
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1f);
|
||||
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
|
||||
contentProviderUtils.insertMarker(new Marker("Marker 1", "Marker 1 desc", "Marker 1 category", null, trackId, service.getLastStoredTrackPointWithLocation(), ""));
|
||||
|
||||
// A sensor-only TrackPoint
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
@@ -156,7 +154,7 @@ public class ExportImportTest {
|
||||
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0f);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
contentProviderUtils.insertMarker(new Marker("Marker 2", "Marker 2 desc", "Marker 2 category", null, trackId, service.getLastStoredTrackPointWithLocation(), ""));
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
|
||||
@@ -596,15 +594,13 @@ public class ExportImportTest {
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
location.setAccuracy(accuracy);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
location.setVerticalAccuracyMeters(verticalAccuracy);
|
||||
}
|
||||
location.setVerticalAccuracyMeters(verticalAccuracy);
|
||||
location.setSpeed(speed);
|
||||
location.setAltitude(altitude);
|
||||
|
||||
mockAltitudeChange(trackPointCreator, altitudeGain);
|
||||
|
||||
trackPointCreator.setClock(time);
|
||||
trackPointCreator.getSensorManager().onChanged(new Raw<>(location));
|
||||
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -166,7 +166,7 @@ public class KMLTrackImporterTest {
|
||||
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkers(importTrackId).size());
|
||||
assertEquals(1, contentProviderUtils.getMarkers(importTrackId).size());
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010 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.services;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackRecordingServiceMarkerTest {
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private TrackRecordingService service;
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
private TrackRecordingService startService() throws TimeoutException {
|
||||
Intent startIntent = new Intent(context, TrackRecordingService.class);
|
||||
return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
|
||||
.getService();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws TimeoutException {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
service = startService();
|
||||
tearDown();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// Ensure that the database is empty after every test
|
||||
contentProviderUtils.deleteAllTracks(context);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void notRecording_testInsertMarker() {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNull(markerId);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_noGPSfix_cannotCreateMarker() {
|
||||
// given
|
||||
service.startNewTrack();
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNull(markerId);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_GPSfix_createsMarker() {
|
||||
// given
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:02Z");
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
service.stopUpdateRecordingData();
|
||||
|
||||
assertTrue(service.isRecording());
|
||||
trackPointCreator.onNewTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
|
||||
.setLatitude(10)
|
||||
.setLongitude(10)
|
||||
);
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNotEquals(new Marker.Id(-1L), markerId);
|
||||
Marker wpt = contentProviderUtils.getMarker(markerId);
|
||||
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
|
||||
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
|
||||
assertEquals(trackId, wpt.getTrackId());
|
||||
assertEquals(0.0, wpt.getLength().toM(), 0.01);
|
||||
assertNotNull(wpt.getLocation());
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
}
|
||||
+61
-2
@@ -1,7 +1,9 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
@@ -231,7 +233,7 @@ public class TrackStatisticsUpdaterTest {
|
||||
));
|
||||
|
||||
// then
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(35), subject.getTrackStatistics().getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -261,10 +263,67 @@ public class TrackStatisticsUpdaterTest {
|
||||
));
|
||||
|
||||
// then
|
||||
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Distance.of(1040), subject.getTrackStatistics().getTotalDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_idle_remain_idle() {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10))
|
||||
.setSensorDistance(Distance.of(10)));
|
||||
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
|
||||
.setSensorDistance(Distance.of(0)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
|
||||
.setSensorDistance(Distance.of(1)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(11), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
|
||||
.setSensorDistance(Distance.of(10)));
|
||||
// then
|
||||
assertFalse(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(50), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60)));
|
||||
|
||||
// then
|
||||
assertFalse(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(60), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copy_constructor() {
|
||||
// given
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.TextStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
@@ -138,10 +137,10 @@ public class StringUtilsTest {
|
||||
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
|
||||
|
||||
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
|
||||
LocalDate yesterday = today.minus(1, ChronoUnit.DAYS);
|
||||
LocalDate dayName = today.minus(2, ChronoUnit.DAYS);
|
||||
LocalDate thisYear = today.minus(15, ChronoUnit.DAYS);
|
||||
LocalDate aYearAgo = today.minus(400, ChronoUnit.DAYS);
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDate dayName = today.minusDays(2);
|
||||
LocalDate thisYear = today.minusDays(15);
|
||||
LocalDate aYearAgo = today.minusDays(400);
|
||||
|
||||
int offsetFromLocale = OffsetDateTime.now().getOffset().getTotalSeconds() - 28800; // -08:00
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ limitations under the License.
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.markers.MarkerDetailActivity"
|
||||
android:name=".publicapi.ShowMarkerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
|
||||
@@ -322,43 +322,24 @@ limitations under the License.
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".publicapi.CreateMarkerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="de.dennisguse.opentracks.CreateMarker" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.markers.MarkerDetailActivity" />
|
||||
<activity android:name=".ui.markers.MarkerEditActivity" />
|
||||
<activity android:name=".ui.markers.MarkerListActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.markers.MarkerListActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/menu_markers"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".TrackListActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".TrackListActivity" />
|
||||
<activity android:name=".TrackRecordedActivity" />
|
||||
<activity android:name=".TrackStoppedActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".TrackRecordingActivity"
|
||||
android:parentActivityName=".TrackListActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".TrackListActivity" />
|
||||
</activity>
|
||||
<activity android:name=".TrackRecordingActivity" />
|
||||
|
||||
<activity android:name=".TrackEditActivity" />
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
@@ -26,7 +25,6 @@ import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -34,7 +32,6 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
@@ -129,7 +126,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
};
|
||||
|
||||
// Menu items
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
private String searchQuery;
|
||||
|
||||
@@ -249,6 +245,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackListBinding.inflate(getLayoutInflater());
|
||||
|
||||
viewBinding.trackListSearchView.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
searchQuery = viewBinding.trackListSearchView.getEditText().getText().toString();
|
||||
viewBinding.trackListSearchView.hide();
|
||||
loadData();
|
||||
return true;
|
||||
});
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@@ -256,9 +260,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -280,13 +281,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_list_search) {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setIconified(false);
|
||||
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_list_help) {
|
||||
startActivity(IntentUtils.newIntent(this, HelpActivity.class));
|
||||
return true;
|
||||
@@ -295,42 +289,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
searchQuery = null;
|
||||
loadData();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
} else {
|
||||
searchQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
viewBinding.trackListToolbar.setText(searchQuery);
|
||||
|
||||
viewBinding.trackListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.app_name)));
|
||||
|
||||
Cursor tracks = new ContentProviderUtils(this).searchTracks(searchQuery);
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackDataHub;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
|
||||
@@ -269,9 +270,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_insert_marker) {
|
||||
TrackPoint trackPoint = trackRecordingServiceConnection.getTrackRecordingService().getLastStoredTrackPointWithLocation();
|
||||
if (trackPoint == null) {
|
||||
return true;
|
||||
}
|
||||
Intent intent = IntentUtils
|
||||
.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getLocation());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,15 +110,6 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
||||
contentProviderUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isDiscarding) {
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
resumeTrackAndFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackStoppedBinding.inflate(getLayoutInflater());
|
||||
|
||||
@@ -41,9 +41,9 @@ import androidx.core.view.GestureDetectorCompat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
@@ -152,31 +152,6 @@ public class ChartView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent event) {
|
||||
// Check if the y event is within markerHeight of the marker center
|
||||
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
Marker nearestMarker = null;
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
int distance = Math.abs(getX(getMarkerXValue(marker)) - (int) event.getX() - getScrollX());
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMarker = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearestMarker != null && minDistance < markerWidth) {
|
||||
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestMarker.getId());
|
||||
getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
private final ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@@ -589,7 +564,6 @@ public class ChartView extends View {
|
||||
|
||||
clipToGraphArea(canvas);
|
||||
drawDataSeries(canvas);
|
||||
drawMarker(canvas);
|
||||
drawGrid(canvas);
|
||||
|
||||
canvas.restore();
|
||||
@@ -630,30 +604,6 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas) {
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
double xValue = getMarkerXValue(marker);
|
||||
double markerIconSizeInXaxisUnits = maxX*markerWidth/effectiveWidth / zoomLevel;
|
||||
if (xValue > maxX + markerIconSizeInXaxisUnits * (1-MARKER_X_ANCHOR)) {
|
||||
continue; // there is no chance that this marker will be visible
|
||||
}
|
||||
canvas.save();
|
||||
float x = getX(getMarkerXValue(marker));
|
||||
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
|
||||
// if marker is not near the end of the track then draw it normally
|
||||
if (xValue < maxX - markerIconSizeInXaxisUnits*(1-MARKER_X_ANCHOR)) {
|
||||
canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
} else { // marker at the end needs to be drawn mirrored so that it is more visible
|
||||
canvas.translate(x + (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
canvas.scale(-1, 1);
|
||||
}
|
||||
markerPin.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the grid.
|
||||
*
|
||||
@@ -679,12 +629,13 @@ public class ChartView extends View {
|
||||
private record TitlePosition(
|
||||
int line, // line number (starts at 1, top to bottom numbering)
|
||||
int xPos // x position in points (starts at 0, left to right indexing)
|
||||
) {};
|
||||
) {}
|
||||
|
||||
private record TitleDimensions(
|
||||
int lineCount, // number of lines the titles will take
|
||||
int lineHeight, // height of a line (all lines have the same height)
|
||||
List<TitlePosition> titlePositions // positions of visible titles (the order corresponds to seriesList)
|
||||
) {};
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Draws series titles.
|
||||
@@ -711,7 +662,7 @@ public class ChartView extends View {
|
||||
private TitleDimensions getTitleDimensions() {
|
||||
int lineCnt = 1;
|
||||
int lineHeight = 0;
|
||||
List<TitlePosition> tps = new ArrayList<TitlePosition>();
|
||||
List<TitlePosition> tps = new ArrayList<>();
|
||||
int xPosInLine = spacer;
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
@@ -1042,14 +993,6 @@ public class ChartView extends View {
|
||||
return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight);
|
||||
}
|
||||
|
||||
private double getMarkerXValue(Marker marker) {
|
||||
if (chartByDistance) {
|
||||
return marker.getLength().toKM_Miles(unitSystem);
|
||||
} else {
|
||||
return marker.getDuration().toMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a paint's Rect for a string.
|
||||
*
|
||||
|
||||
@@ -377,8 +377,6 @@ public class ContentProviderUtils {
|
||||
int categoryIndex = cursor.getColumnIndexOrThrow(MarkerColumns.CATEGORY);
|
||||
int iconIndex = cursor.getColumnIndexOrThrow(MarkerColumns.ICON);
|
||||
int trackIdIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TRACKID);
|
||||
int lengthIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LENGTH);
|
||||
int durationIndex = cursor.getColumnIndexOrThrow(MarkerColumns.DURATION);
|
||||
int longitudeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LONGITUDE);
|
||||
int latitudeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LATITUDE);
|
||||
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
|
||||
@@ -419,13 +417,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(iconIndex)) {
|
||||
marker.setIcon(cursor.getString(iconIndex));
|
||||
}
|
||||
if (!cursor.isNull(lengthIndex)) {
|
||||
marker.setLength(Distance.of(cursor.getFloat(lengthIndex)));
|
||||
}
|
||||
if (!cursor.isNull(durationIndex)) {
|
||||
marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex)));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(photoUrlIndex)) {
|
||||
marker.setPhotoUrl(cursor.getString(photoUrlIndex));
|
||||
}
|
||||
@@ -542,9 +533,6 @@ public class ContentProviderUtils {
|
||||
values.put(MarkerColumns.CATEGORY, marker.getCategory());
|
||||
values.put(MarkerColumns.ICON, marker.getIcon());
|
||||
values.put(MarkerColumns.TRACKID, marker.getTrackId().id());
|
||||
values.put(MarkerColumns.LENGTH, marker.getLength().toM());
|
||||
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
|
||||
|
||||
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
|
||||
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
|
||||
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
|
||||
|
||||
@@ -29,7 +29,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 37;
|
||||
private static final int DATABASE_VERSION = 38;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -81,6 +81,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 35 -> upgradeFrom34to35(db);
|
||||
case 36 -> upgradeFrom35to36(db);
|
||||
case 37 -> upgradeFrom36to37(db);
|
||||
case 38 -> upgradeFrom37to38(db);
|
||||
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +106,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 34 -> downgradeFrom35to34(db);
|
||||
case 35 -> downgradeFrom36to35(db);
|
||||
case 36 -> downgradeFrom37to36(db);
|
||||
case 37 -> downgradeFrom38to37(db);
|
||||
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
@@ -644,4 +646,28 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void upgradeFrom37to38(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE markers RENAME TO markers_old");
|
||||
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO markers SELECT _id, name, description, category, icon, trackid, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers_old");
|
||||
db.execSQL("DROP TABLE markers_old");
|
||||
|
||||
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom38to37(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE markers ADD COLUMN length FLOAT");
|
||||
db.execSQL("ALTER TABLE markers ADD COLUMN duration INTEGER");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public enum ActivityType {
|
||||
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_mtb_24dp, true),
|
||||
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true),
|
||||
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
|
||||
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
|
||||
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
|
||||
OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true),
|
||||
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true),
|
||||
PARA_GLIDING("para gliding", R.string.activity_type_para_gliding, ActivityIcon.ICON_UNKNOWN, true),
|
||||
|
||||
@@ -27,8 +27,6 @@ import androidx.annotation.Nullable;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
|
||||
*
|
||||
@@ -53,10 +51,6 @@ public final class Marker {
|
||||
private Altitude altitude;
|
||||
private Float bearing;
|
||||
|
||||
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||
private Distance length;
|
||||
private Duration duration;
|
||||
|
||||
@Deprecated //TODO Make an URI instead of String
|
||||
private String photoUrl = "";
|
||||
|
||||
@@ -65,7 +59,6 @@ public final class Marker {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Deprecated //TODO Marker cannot be created without length AND duration!
|
||||
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this.trackId = trackId;
|
||||
|
||||
@@ -75,20 +68,15 @@ public final class Marker {
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
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) {
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
this.icon = icon;
|
||||
this.length = statistics.getTotalDistance();
|
||||
this.duration = statistics.getTotalTime();
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
@@ -234,22 +222,6 @@ public final class Marker {
|
||||
this.bearing = bearing;
|
||||
}
|
||||
|
||||
public Distance getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(Distance length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(@NonNull Duration duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getPhotoUrl() {
|
||||
return photoUrl;
|
||||
}
|
||||
@@ -263,7 +235,7 @@ public final class Marker {
|
||||
}
|
||||
|
||||
public boolean hasPhoto() {
|
||||
return photoUrl != null && !"".equals(photoUrl);
|
||||
return photoUrl != null && !photoUrl.isEmpty();
|
||||
}
|
||||
|
||||
public record Id(long id) implements Parcelable {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
//TODO Use everywhere instead of android.location.Location.
|
||||
public record Position(
|
||||
Instant time,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
Distance horizontalAccuracy,
|
||||
Altitude altitude,
|
||||
Distance verticalAccuracy,
|
||||
Float bearing,
|
||||
Speed speed
|
||||
) {
|
||||
public static Position empty() {
|
||||
return new Position(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static Position of(Location location) {
|
||||
return new Position(
|
||||
Instant.ofEpochMilli(location.getTime()),
|
||||
location.getLatitude(),
|
||||
location.getLongitude(),
|
||||
location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null,
|
||||
location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null,
|
||||
location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null,
|
||||
location.hasBearing() ? location.getBearing() : null,
|
||||
location.hasSpeed() ? Speed.of(location.getSpeed()) : null
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null && longitude != null;
|
||||
}
|
||||
|
||||
public boolean hasHorizontalAccuracy() {
|
||||
return horizontalAccuracy != null;
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude != null;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return speed != null;
|
||||
}
|
||||
|
||||
public Location toLocation() {
|
||||
if (!hasLocation()) {
|
||||
throw new RuntimeException("Cannot convert to Location.");
|
||||
}
|
||||
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
if (hasBearing()) {
|
||||
location.setBearing(bearing);
|
||||
}
|
||||
if (hasHorizontalAccuracy()) {
|
||||
location.setAccuracy((float) horizontalAccuracy.toM());
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
if (hasSpeed()) {
|
||||
location.setSpeed((float) speed.toMPS());
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
@@ -26,6 +25,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Sensor and/or location information for a specific point in time.
|
||||
@@ -42,6 +42,7 @@ public class TrackPoint {
|
||||
@NonNull
|
||||
private final Instant time;
|
||||
|
||||
//TODO We may use Position for these items
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Distance horizontalAccuracy;
|
||||
@@ -135,6 +136,10 @@ public class TrackPoint {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isIdleTriggered() {
|
||||
return type == Type.IDLE;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualStart() {
|
||||
return type == Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
@@ -181,42 +186,40 @@ public class TrackPoint {
|
||||
return this;
|
||||
}
|
||||
|
||||
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
if (hasLocation()) {
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
}
|
||||
if (hasBearing()) {
|
||||
location.setBearing(bearing);
|
||||
}
|
||||
if (hasHorizontalAccuracy()) {
|
||||
location.setAccuracy((float) horizontalAccuracy.toM());
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
if (hasSpeed()) {
|
||||
location.setSpeed((float) speed.toMPS());
|
||||
}
|
||||
|
||||
return location;
|
||||
public Position getPosition() {
|
||||
return new Position(
|
||||
time,
|
||||
latitude,
|
||||
longitude,
|
||||
horizontalAccuracy,
|
||||
altitude,
|
||||
verticalAccuracy,
|
||||
bearing,
|
||||
speed
|
||||
);
|
||||
}
|
||||
|
||||
public TrackPoint setLocation(@NonNull Location location) {
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
|
||||
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
|
||||
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
|
||||
}
|
||||
public TrackPoint setPosition(Position location) {
|
||||
this.latitude = location.latitude();
|
||||
this.longitude = location.longitude();
|
||||
this.altitude = location.altitude();
|
||||
this.speed = location.speed();
|
||||
this.horizontalAccuracy = location.horizontalAccuracy();
|
||||
this.verticalAccuracy = location.verticalAccuracy();
|
||||
|
||||
//TODO Should we copy the bearing?
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
return getPosition().toLocation();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public TrackPoint setLocation(@NonNull Location location) {
|
||||
setPosition(Position.of(location));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -288,6 +291,7 @@ public class TrackPoint {
|
||||
this.speed = speed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
@@ -348,13 +352,20 @@ public class TrackPoint {
|
||||
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
|
||||
}
|
||||
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
|
||||
if (!dest.hasLocation() || !hasLocation()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return 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);
|
||||
@Deprecated
|
||||
public Optional<Float> bearingTo(@NonNull Location dest) {
|
||||
if (!hasLocation()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(getLocation().bearingTo(dest));
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
|
||||
@@ -41,10 +41,6 @@ public interface MarkerColumns extends BaseColumns {
|
||||
String CATEGORY = "category"; // marker category
|
||||
String ICON = "icon"; // marker icon
|
||||
String TRACKID = "trackid"; // track id
|
||||
|
||||
String LENGTH = "length"; // length of the track (without smoothing)
|
||||
String DURATION = "duration"; // total duration of the track from the beginning until now
|
||||
|
||||
String LONGITUDE = "longitude"; // longitude
|
||||
String LATITUDE = "latitude"; // latitude
|
||||
String TIME = "time"; // time
|
||||
@@ -61,8 +57,6 @@ public interface MarkerColumns extends BaseColumns {
|
||||
+ CATEGORY + " TEXT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
+ LENGTH + " FLOAT, "
|
||||
+ DURATION + " INTEGER, "
|
||||
+ LONGITUDE + " INTEGER, "
|
||||
+ LATITUDE + " INTEGER, "
|
||||
+ TIME + " INTEGER, "
|
||||
|
||||
@@ -310,7 +310,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
public Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
|
||||
Distance cumulativeDistance = null;
|
||||
Distance cumulativeDistance;
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -274,7 +275,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
float heading = getHeading(marker.getTrackId(), marker.getLocation());
|
||||
writePhotoOverlay(marker, heading, zoneOffset);
|
||||
} else {
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), zoneOffset);
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), marker.getTime(), zoneOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +369,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
@VisibleForTesting
|
||||
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
|
||||
|
||||
trackpointTypeList.add(trackPoint.getType());
|
||||
|
||||
@@ -411,12 +412,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.println("</SimpleArrayData>");
|
||||
}
|
||||
|
||||
private void writePlacemark(String name, String activityType, String description, Location location, ZoneOffset zoneOffset) {
|
||||
private void writePlacemark(String name, String activityType, String description, Location location, Instant time, ZoneOffset zoneOffset) {
|
||||
if (location != null) {
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, time) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
|
||||
writeTypeLocalized(activityType);
|
||||
printWriter.println("<Point>");
|
||||
@@ -437,7 +438,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.print("<heading>" + heading + "</heading>");
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getTime()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
|
||||
writeTypeLocalized(marker.getCategory());
|
||||
|
||||
@@ -460,14 +461,13 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
/**
|
||||
* Returns the formatted time of the location; either absolute or relative depending exportTrackDetail.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private String getTime(ZoneOffset zoneOffset, Location location) {
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()), zoneOffset);
|
||||
private String getTime(ZoneOffset zoneOffset, Instant instant) {
|
||||
return StringUtils.formatDateTimeIso8601(instant, zoneOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: check if this is a useful feature (likely not).
|
||||
* Gets the heading to a location.
|
||||
*
|
||||
* @param trackId the track id containing the location
|
||||
@@ -480,7 +480,10 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
TrackPoint viewLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
if (viewLocation != null) {
|
||||
return viewLocation.bearingTo(location);
|
||||
Optional<Float> bearing = viewLocation.bearingTo(location);
|
||||
if (bearing.isPresent()) {
|
||||
return bearing.get();
|
||||
}
|
||||
}
|
||||
|
||||
return location.getBearing();
|
||||
@@ -495,7 +498,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
private void writeTypeLocalized(String localizedValue) {
|
||||
if (localizedValue == null || localizedValue.equals("")) {
|
||||
if (localizedValue == null || localizedValue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
printWriter.println("<ExtendedData>");
|
||||
|
||||
@@ -414,7 +414,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
content = content.trim();
|
||||
if (!"".equals(content)) {
|
||||
if (!content.isEmpty()) {
|
||||
try {
|
||||
value = Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.util.Collections;
|
||||
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.data.ContentProviderUtils;
|
||||
@@ -26,7 +25,6 @@ import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
@@ -158,7 +156,7 @@ public class TrackImporter {
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
|
||||
|
||||
// Store Markers
|
||||
matchMarkers2TrackPoints(trackId);
|
||||
updateMarkers(trackId);
|
||||
for (Marker marker : markers)
|
||||
marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers
|
||||
|
||||
@@ -206,7 +204,8 @@ public class TrackImporter {
|
||||
}
|
||||
|
||||
if (!current.hasBearing()) {
|
||||
current.setBearing(previous.bearingTo(current));
|
||||
previous.bearingTo(current)
|
||||
.ifPresent(current::setBearing);
|
||||
}
|
||||
|
||||
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||
@@ -217,55 +216,16 @@ public class TrackImporter {
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Modifies content of markers (incl. removal).
|
||||
* NOTE: Modifies content of markers.
|
||||
*/
|
||||
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;
|
||||
private void updateMarkers(Track.Id trackId) {
|
||||
markers.forEach(marker -> {
|
||||
if (marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
|
||||
}
|
||||
|
||||
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||
updater.addTrackPoint(trackPoint);
|
||||
|
||||
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);
|
||||
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* Public API to creates a Marker for a given track with a given location
|
||||
*/
|
||||
public class CreateMarkerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_LOCATION = "location";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Track.Id trackId = new Track.Id(getIntent().getLongExtra(EXTRA_TRACK_ID, 0L));
|
||||
Location location = getIntent().getParcelableExtra(EXTRA_LOCATION);
|
||||
|
||||
TrackRecordingServiceConnection.execute(this, (service, self) -> {
|
||||
Intent intent = IntentUtils
|
||||
.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, location);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.databinding.MarkerDetailActivityBinding;
|
||||
import de.dennisguse.opentracks.ui.markers.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailFragment;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* Public api to show an existing marker
|
||||
*/
|
||||
public class ShowMarkerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String TAG = ShowMarkerActivity.class.getSimpleName();
|
||||
|
||||
private MarkerDetailActivityBinding viewBinding;
|
||||
|
||||
private List<Marker.Id> markerIds;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Marker.Id markerId = new Marker.Id(getIntent().getLongExtra(EXTRA_MARKER_ID, 0));
|
||||
if (markerId.id() == 0) {
|
||||
throw new IllegalStateException("No valid markerId provided");
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,9 +20,7 @@ public class GainManager implements SensorConnector {
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver listener;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
connect();
|
||||
};
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> connect();
|
||||
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
|
||||
@@ -55,7 +55,7 @@ public abstract class Aggregator<Input, Output> {
|
||||
/**
|
||||
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
|
||||
*/
|
||||
public void reset() {};
|
||||
public void reset() {}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
|
||||
public class AggregatorGPS extends Aggregator<Location, Optional<Location>> {
|
||||
public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
|
||||
|
||||
public AggregatorGPS(String sensorAddress) {
|
||||
@@ -14,8 +12,8 @@ public class AggregatorGPS extends Aggregator<Location, Optional<Location>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<Location> current) {
|
||||
value = Optional.of(current.value());
|
||||
protected void computeValue(Raw<Position> current) {
|
||||
value = current.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,7 +23,7 @@ public class AggregatorGPS extends Aggregator<Location, Optional<Location>> {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Optional<Location> getNoneValue() {
|
||||
return Optional.empty();
|
||||
protected Position getNoneValue() {
|
||||
return Position.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
@@ -11,6 +10,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
@@ -127,8 +127,8 @@ public final class SensorDataSet {
|
||||
this.barometer.add((Raw<AtmosphericPressure>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Location) {
|
||||
this.gps.add((Raw<Location>) data);
|
||||
if (value instanceof Position) {
|
||||
this.gps.add((Raw<Position>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,8 +152,7 @@ public final class SensorDataSet {
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
if (gps != null && gps.hasValue()) {
|
||||
gps.getValue()
|
||||
.ifPresent(trackPoint::setLocation);
|
||||
trackPoint.setPosition(gps.getValue());
|
||||
}
|
||||
|
||||
if (getHeartRate() != null) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -19,7 +17,6 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -138,31 +135,6 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return new Pair<>(track, current);
|
||||
}
|
||||
|
||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||
if (name == null) {
|
||||
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(trackId);
|
||||
if (nextMarkerNumber == null) {
|
||||
nextMarkerNumber = 1;
|
||||
}
|
||||
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
|
||||
}
|
||||
|
||||
if (lastStoredTrackPointWithLocation == null) {
|
||||
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
|
||||
return null;
|
||||
}
|
||||
|
||||
category = category != null ? category : "";
|
||||
description = description != null ? description : "";
|
||||
String icon = context.getString(R.string.marker_icon_url);
|
||||
photoUrl = photoUrl != null ? photoUrl : "";
|
||||
|
||||
// Insert marker
|
||||
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPointWithLocation, photoUrl);
|
||||
Uri uri = contentProviderUtils.insertMarker(marker);
|
||||
return new Marker.Id(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public void onIdle() {
|
||||
Log.d(TAG, "Becoming idle");
|
||||
@@ -247,7 +219,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
|
||||
TrackStatistics getTrackStatistics() {
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
return trackStatisticsUpdater == null ? null : trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
|
||||
private void insertTrackPoint(@NonNull TrackPoint trackPoint, boolean storeLastTrackPointIfUseful) {
|
||||
@@ -308,6 +280,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return lastStoredTrackPointWithLocation;
|
||||
}
|
||||
|
||||
public interface IdleObserver {
|
||||
void onIdle();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -57,6 +56,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
|
||||
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return trackRecordingManager.getLastStoredTrackPointWithLocation();
|
||||
}
|
||||
|
||||
public class Binder extends android.os.Binder {
|
||||
|
||||
private Binder() {
|
||||
@@ -269,14 +272,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
gpsStatusObservable.postValue(gpsStatusValue);
|
||||
}
|
||||
|
||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||
if (!isRecording()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trackRecordingManager.insertMarker(name, category, description, photoUrl);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public TrackPointCreator getTrackPointCreator() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.SensorConnector;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
@@ -138,7 +139,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
return;
|
||||
}
|
||||
|
||||
listener.onChange(new Raw<>(location));
|
||||
listener.onChange(new Raw<>(Position.of(location)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -229,7 +229,7 @@ public class PreferencesUtils {
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").equals("")) {
|
||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||
if (!Locale.US.equals(Locale.getDefault())) {
|
||||
setUnit(UnitSystem.METRIC);
|
||||
} else {
|
||||
|
||||
@@ -42,8 +42,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
fragment = new MainSettingsFragment();
|
||||
}
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
setSupportActionBar(viewBinding.topAppBar);
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, fragment).commit();
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ public class TrackStatistics {
|
||||
*
|
||||
* @param other another statistics data object
|
||||
*/
|
||||
//TODO Should be refactored to append only [mainly due to isIdle] (NOTE: This requires to use a custom value object for AggregatedStatistics; this is anyhow recommended).
|
||||
public void merge(TrackStatistics other) {
|
||||
if (startTime == null) {
|
||||
startTime = other.startTime;
|
||||
@@ -119,6 +120,8 @@ public class TrackStatistics {
|
||||
stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime;
|
||||
}
|
||||
|
||||
isIdle = other.isIdle; //TODO This implicitly assumes append mode.
|
||||
|
||||
if (avgHeartRate == null) {
|
||||
avgHeartRate = other.avgHeartRate;
|
||||
} else {
|
||||
|
||||
@@ -26,6 +26,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Updater for {@link TrackStatistics}.
|
||||
@@ -56,11 +57,6 @@ public class TrackStatisticsUpdater {
|
||||
this(new TrackStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new{@link TrackStatisticsUpdater} with a {@link TrackStatisticsUpdater} already existed.
|
||||
*
|
||||
* @param trackStatistics a {@link TrackStatisticsUpdater}
|
||||
*/
|
||||
public TrackStatisticsUpdater(TrackStatistics trackStatistics) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.currentSegment = new TrackStatistics();
|
||||
@@ -87,9 +83,6 @@ public class TrackStatisticsUpdater {
|
||||
trackPoints.stream().forEachOrdered(this::addTrackPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void addTrackPoint(TrackPoint trackPoint) {
|
||||
if (trackPoint.isSegmentManualStart()) {
|
||||
reset(trackPoint);
|
||||
@@ -151,18 +144,23 @@ public class TrackStatisticsUpdater {
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
if (movingDistance != null) {
|
||||
currentSegment.setIdle(false);
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!currentSegment.isIdle() && !trackPoint.isSegmentManualStart()) {
|
||||
if (lastTrackPoint != null) {
|
||||
if (!currentSegment.isIdle()) {
|
||||
if (!trackPoint.isSegmentManualStart() && lastTrackPoint != null) {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
|
||||
if (trackPoint.isIdleTriggered()) {
|
||||
currentSegment.setIdle(true);
|
||||
} else if (currentSegment.isIdle()) {
|
||||
// Shall we switch to non-idle?
|
||||
if (movingDistance != null
|
||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||
currentSegment.setIdle(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.hasSpeed()) {
|
||||
|
||||
@@ -59,15 +59,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
|
||||
Marker.Id markerId = null;
|
||||
if (bundleMarkerId instanceof Marker.Id cast) {
|
||||
markerId = cast;
|
||||
}
|
||||
if (bundleMarkerId instanceof Long cast) {
|
||||
//Incoming Intent via Dashboard API.
|
||||
markerId = new Marker.Id(cast);
|
||||
}
|
||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId == null) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
finish();
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
@@ -36,19 +37,19 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.PickVisualMediaRequest;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
/**
|
||||
* An activity to add/edit a marker.
|
||||
@@ -59,13 +60,14 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
public static final String EXTRA_LOCATION = "location";
|
||||
|
||||
private static final String CAMERA_PHOTO_URI_KEY = "camera_photo_uri_key";
|
||||
|
||||
private static final String NEW_MARKER_ID = "new_marker_id";
|
||||
|
||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||
private Track.Id trackId;
|
||||
private Location location;
|
||||
private Marker.Id markerId;
|
||||
private Marker marker;
|
||||
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
@@ -93,16 +95,16 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
@Nullable Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
final boolean isNewMarker = markerId == null;
|
||||
location = getIntent().getParcelableExtra(EXTRA_LOCATION);
|
||||
markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if ((trackId == null || location == null) && markerId == null) {
|
||||
throw new IllegalStateException("TrackId and Location must be provided or an existing markerId");
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
cameraPhotoUri = Uri.parse(savedInstanceState.getString(CAMERA_PHOTO_URI_KEY, ""));
|
||||
Marker.Id newMarkerId = savedInstanceState.getParcelable(NEW_MARKER_ID);
|
||||
if (newMarkerId != null) {
|
||||
markerId = newMarkerId;
|
||||
}
|
||||
}
|
||||
boolean isNewMarker = markerId == null;
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
|
||||
@@ -157,16 +159,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (markerId == null) {
|
||||
TrackRecordingServiceConnection.execute(this, (service, self) -> {
|
||||
Marker.Id newMarkerId = createNewMarker(service);
|
||||
if (newMarkerId == null) {
|
||||
finish();
|
||||
} else {
|
||||
loadMarkerData(newMarkerId);
|
||||
}
|
||||
});
|
||||
if (isNewMarker) {
|
||||
createNewMarker().observe(this, this::loadMarkerData);
|
||||
} else {
|
||||
loadMarkerData(markerId);
|
||||
}
|
||||
@@ -174,35 +168,27 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
}
|
||||
|
||||
private Marker.Id createNewMarker(TrackRecordingService trackRecordingService) {
|
||||
try {
|
||||
Marker.Id marker = trackRecordingService.insertMarker("", "", "", null);
|
||||
if (marker == null) {
|
||||
Toast.makeText(this, R.string.marker_add_error, Toast.LENGTH_LONG).show();
|
||||
return null;
|
||||
}
|
||||
|
||||
return marker;
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "Unable to add marker.", e);
|
||||
return null;
|
||||
}
|
||||
private LiveData<Marker> createNewMarker() {
|
||||
TrackPoint trackPoint = new TrackPoint(location, Instant.now());
|
||||
return viewModel.createNewMarker(trackId, trackPoint);
|
||||
}
|
||||
|
||||
private void loadMarkerData(Marker.Id markerId) {
|
||||
viewModel.getMarkerData(markerId).observe(this, data -> {
|
||||
marker = data;
|
||||
viewBinding.markerEditName.setText(marker.getName());
|
||||
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||
if (marker.hasPhoto()) {
|
||||
setMarkerImageView(marker.getPhotoURI());
|
||||
} else {
|
||||
viewBinding.markerEditPhoto.setImageDrawable(null);
|
||||
}
|
||||
viewModel.getMarkerData(markerId).observe(this, this::loadMarkerData);
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
});
|
||||
private void loadMarkerData(Marker data) {
|
||||
marker = data;
|
||||
viewBinding.markerEditName.setText(marker.getName());
|
||||
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||
if (marker.hasPhoto()) {
|
||||
setMarkerImageView(marker.getPhotoURI());
|
||||
} else {
|
||||
viewBinding.markerEditPhoto.setImageDrawable(null);
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,6 +196,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
super.onDestroy();
|
||||
|
||||
trackId = null;
|
||||
location = null;
|
||||
markerId = null;
|
||||
viewBinding = null;
|
||||
viewModel = null;
|
||||
takePictureFromGallery = null;
|
||||
@@ -222,8 +210,6 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
if (cameraPhotoUri != null) {
|
||||
outState.putString(CAMERA_PHOTO_URI_KEY, cameraPhotoUri.toString());
|
||||
}
|
||||
|
||||
outState.putParcelable(NEW_MARKER_ID, marker.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.NoSuchElementException;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class MarkerEditViewModel extends AndroidViewModel {
|
||||
@@ -122,22 +124,40 @@ public class MarkerEditViewModel extends AndroidViewModel {
|
||||
marker.setName(name);
|
||||
marker.setCategory(category);
|
||||
marker.setDescription(description);
|
||||
new ContentProviderUtils(getApplication()).updateMarker(getApplication(), marker);
|
||||
if (marker.getId() == null) {
|
||||
new ContentProviderUtils(getApplication()).insertMarker(marker);
|
||||
} else {
|
||||
new ContentProviderUtils(getApplication()).updateMarker(getApplication(), marker);
|
||||
}
|
||||
|
||||
if (photoOriginalUri != null && (!marker.hasPhoto() || !photoOriginalUri.equals(marker.getPhotoURI()))) {
|
||||
deletePhoto(photoOriginalUri);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<Marker> createNewMarker(Track.Id trackId, TrackPoint trackPoint) {
|
||||
Integer nextMarkerNumber = new ContentProviderUtils(getApplication()).getNextMarkerNumber(trackId);
|
||||
if (nextMarkerNumber == null) {
|
||||
nextMarkerNumber = 1;
|
||||
}
|
||||
String name = getApplication().getString(R.string.marker_name_format, nextMarkerNumber + 1);
|
||||
String icon = getApplication().getString(R.string.marker_icon_url);
|
||||
|
||||
Marker marker = new Marker(name, "", "", icon, trackId, trackPoint, "");
|
||||
|
||||
if (markerData == null) {
|
||||
markerData = new MutableLiveData<>();
|
||||
}
|
||||
markerData.postValue(marker);
|
||||
return markerData;
|
||||
}
|
||||
|
||||
public void onCancel(boolean isNewMarker) {
|
||||
Marker marker = getMarker();
|
||||
if (isNewMarker) {
|
||||
// it's new marker -> clean all photos.
|
||||
deletePhoto(marker);
|
||||
deletePhoto(photoOriginalUri);
|
||||
|
||||
new ContentProviderUtils(getApplication()).deleteMarker(getApplication(), marker.getId());
|
||||
|
||||
} else if (photoOriginalUri == null || (marker.hasPhoto() && !marker.getPhotoURI().equals(photoOriginalUri))) {
|
||||
// it's an edit marker -> delete photo if it was empty or it was changed (leaving the original in that case).
|
||||
deletePhoto(marker);
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks.ui.markers;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
@@ -25,7 +24,6 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import java.util.List;
|
||||
@@ -95,7 +93,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
}
|
||||
};
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
private String searchQuery;
|
||||
|
||||
@@ -117,9 +114,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
setSupportActionBar(viewBinding.markerListToolbar);
|
||||
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationOnClickListener(item -> {
|
||||
finish();
|
||||
});
|
||||
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationOnClickListener(item -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,6 +148,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
|
||||
|
||||
viewBinding.markerListSearchView.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
searchQuery = viewBinding.markerListSearchView.getEditText().getText().toString();
|
||||
viewBinding.markerListSearchView.hide();
|
||||
loadData();
|
||||
return true;
|
||||
});
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@@ -162,9 +165,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -236,35 +236,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
searchQuery = null;
|
||||
loadData();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
} else {
|
||||
searchQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void loadData() {
|
||||
viewBinding.markerListToolbar.setText(searchQuery);
|
||||
|
||||
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
|
||||
|
||||
List<Marker> markers = contentProviderUtils.searchMarkers(trackId, searchQuery);
|
||||
|
||||
@@ -2,18 +2,13 @@ package de.dennisguse.opentracks.ui.util;
|
||||
|
||||
import static android.content.Context.VIBRATOR_SERVICE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@@ -21,19 +16,6 @@ public class ActivityUtils {
|
||||
|
||||
private static final String TAG = ActivityUtils.class.getSimpleName();
|
||||
|
||||
public static SearchView configureSearchWidget(Activity activity, final MenuItem menuItem) {
|
||||
final SearchView searchView = (SearchView) menuItem.getActionView();
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
//NOTE: Use searchManager.getSearchableInfo(new ComponentName(activity, SearchActivity.class)) if another activity should handle the search
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName()));
|
||||
} else {
|
||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||
}
|
||||
searchView.setSubmitButtonEnabled(true);
|
||||
return searchView;
|
||||
}
|
||||
|
||||
public static void vibrate(@NonNull Context context, Duration duration) {
|
||||
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="5.8208"
|
||||
android:viewportHeight="5.8208">
|
||||
<path
|
||||
|
||||
@@ -22,14 +22,24 @@ limitations under the License.
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<com.google.android.material.search.SearchBar
|
||||
android:id="@+id/marker_list_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_marker_show_24dp"
|
||||
app:title="@string/menu_markers" />
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior"
|
||||
app:menu="@menu/marker_list" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
android:id="@+id/marker_list_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/menu_search"
|
||||
app:layout_anchor="@id/marker_list_toolbar">
|
||||
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
|
||||
</com.google.android.material.search.SearchView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -30,7 +30,7 @@ limitations under the License.
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="?attr/textInputOutlinedStyle"
|
||||
style="?attr/textInputOutlinedExposedDropdownMenuStyle"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
@@ -22,15 +22,24 @@ limitations under the License.
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<com.google.android.material.search.SearchBar
|
||||
android:id="@+id/track_list_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_logo_color_24dp"
|
||||
app:title="@string/app_name"
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior"
|
||||
app:menu="@menu/track_list" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
android:id="@+id/track_list_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/menu_search"
|
||||
app:layout_anchor="@id/track_list_toolbar">
|
||||
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
|
||||
</com.google.android.material.search.SearchView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -17,12 +17,7 @@ limitations under the License.
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/marker_list_insert_marker"
|
||||
android:icon="@drawable/ic_marker_show_24dp"
|
||||
android:icon="@drawable/ic_marker_add_24dp"
|
||||
android:title="@string/menu_insert_marker"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/marker_list_search"
|
||||
android:title="@string/menu_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -15,12 +15,6 @@ limitations under the License.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
|
||||
<item
|
||||
android:id="@+id/track_list_search"
|
||||
android:title="@string/menu_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_markers"
|
||||
android:icon="@drawable/ic_marker_show_24dp"
|
||||
@@ -29,7 +23,8 @@ limitations under the License.
|
||||
<item
|
||||
android:id="@+id/track_list_settings"
|
||||
android:icon="@drawable/ic_settings_24dp"
|
||||
android:title="@string/menu_settings" />
|
||||
android:title="@string/menu_settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_help"
|
||||
android:icon="@drawable/ic_help_24dp"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="activity_type_airplane">aeroplane</string>
|
||||
<string name="activity_type_atv">ATV</string>
|
||||
<string name="activity_type_biking">biking</string>
|
||||
<string name="activity_type_atv">ATV 4-Wheeler</string>
|
||||
<string name="activity_type_biking">A bike ride</string>
|
||||
<string name="activity_type_blimp">airship</string>
|
||||
<string name="activity_type_boat">boat</string>
|
||||
<string name="activity_type_boat">Boating</string>
|
||||
<string name="activity_type_climbing">climbing</string>
|
||||
<string name="activity_type_commercial_airplane">commercial aeroplane</string>
|
||||
<string name="activity_type_commercial_airplane">commercial flight</string>
|
||||
<string name="activity_type_cross_country_skiing">Cross-country skiing</string>
|
||||
<string name="activity_type_cycling">cycling</string>
|
||||
<string name="activity_type_dirt_bike">dirt bike</string>
|
||||
<string name="activity_type_donkey_back_riding">donkey riding</string>
|
||||
<string name="activity_type_donkey_back_riding">Donkey ride</string>
|
||||
<string name="activity_type_driving">driving</string>
|
||||
<string name="activity_type_driving_bus">driving bus</string>
|
||||
<string name="activity_type_driving_car">driving car</string>
|
||||
<string name="activity_type_driving_car">Car driving</string>
|
||||
<string name="activity_type_ferry">ferry</string>
|
||||
<string name="activity_type_frisbee">frisbee</string>
|
||||
<string name="activity_type_gliding">gliding</string>
|
||||
|
||||
@@ -419,14 +419,14 @@ limitations under the License.
|
||||
<item quantity="other">Ukončeno s %1$d chybami</item>
|
||||
</plurals>
|
||||
<string name="import_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Zabraňte opětovnému importu aktivit</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Zabránit opětov. importu stop</string>
|
||||
<string name="export_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="export_progress_summary_skip_msg">Soubory přeskočeny</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Soubory přepsány</string>
|
||||
<string name="export_progress_summary_new_msg">Nové soubory exportovány</string>
|
||||
<string name="import_progress_summary_exists_msg">Existovat</string>
|
||||
<string name="import_progress_summary_ok_msg">Importované soubory</string>
|
||||
<string name="generic_open_track">Otevřená trať</string>
|
||||
<string name="generic_open_track">Otevřít stopu</string>
|
||||
<string name="sensor_state_power_avg">Prům. výkon</string>
|
||||
<string name="sensor_state_heart_rate_max">Maximální srdeční frekvence</string>
|
||||
<string name="sensor_state_heart_rate_avg">Avg srdeční frekvence</string>
|
||||
@@ -608,4 +608,5 @@ limitations under the License.
|
||||
<string name="power">Výkon</string>
|
||||
<string name="settings_announcements_lap_power">Výkon kola</string>
|
||||
<string name="power_x_watt">Výkon {x, plural, =1 {1 watt} few {# watty} other {# wattů} }</string>
|
||||
<string name="create_marker_error">Nepodařilo se vytvořit značku. Zkuste to znovu.</string>
|
||||
</resources>
|
||||
@@ -641,4 +641,9 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="settings_announcements_unit">Distanz/Geschwindigkeits Einheit</string>
|
||||
<string name="power_x_watt">Leistung {x, plural, =1 {1 Watt} other {# Watt} }</string>
|
||||
<string name="power">Leistung</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } anderes {{n,number,#.0}} }</string>
|
||||
<string name="settings_recording_min_sampling_interval_title">Abtastzeitintervall</string>
|
||||
<string name="voiceIdle">Untätig werden.</string>
|
||||
<string name="voice_announce_lap_power_key">StimmeAnkündigungLapPower</string>
|
||||
<string name="settings_announcements_lap_power">Rundenleistung</string>
|
||||
</resources>
|
||||
@@ -37,12 +37,12 @@ limitations under the License.
|
||||
<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 recorridos grabados\?</string>
|
||||
<string name="help_track_export_content"> \u0020Los Tracks se pueden exportar desde la Configuración como KMZ, KML o GPX. \u0020KMZ/KML todos los datos; solo ubicaciones GPX. \u0020Las pistas compartidas a través del <i>icono de compartir</i> se comparten como KMZ. \u0020</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; Ubicaciones solo GPX. 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 los servicios de 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_data_storage_content"> \u0020Los datos del sensor se almacenan por ubicación registrada. \u0020En términos de un sensor de frecuencia cardiaca, esto significa que sólo se almacena <i>una</i> medición de frecuencia cardiaca por ubicación. \u0020Si está parado o se encuentra en interiores, no se registrará ningún dato del sensor (pero se mostrará). \u0020</string>
|
||||
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de un 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á en interiores, no se registrarán los datos del sensor (pero se mostrarán). </string>
|
||||
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
|
||||
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer:
|
||||
\na) compruebe si el GPS está habilitado,
|
||||
@@ -657,4 +657,11 @@ limitations under the License.
|
||||
<string name="settings_theme_switch_restart">Los cambios del tema pueden requerir un reinicio manual.</string>
|
||||
<string name="settings_night_mode_option_night_oled">Oscuro (compatible con OLED)</string>
|
||||
<string name="settings_announcements_unit">Unidad de distancia/velocidad</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
|
||||
<string name="settings_announcements_lap_power">Potencia por vuelta</string>
|
||||
<string name="power_x_watt">Potencia {x, plural, =1 {1 vatio} otros {# vatios} }</string>
|
||||
<string name="create_marker_error">No se ha podido crear un marcador. Inténtelo de nuevo.</string>
|
||||
<string name="voice_announce_lap_power_key">voiceAnnounceLapPower</string>
|
||||
<string name="power">Potencia</string>
|
||||
</resources>
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
--><resources>
|
||||
<string name="about_preference_title">À propos d\'OpenTracks</string>
|
||||
<string name="about_description"><i>OpenTracks</i> est une application de suivi sportif qui respecte totalement votre vie privée.</string>
|
||||
<string name="about_url">Page web du projet : <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_url">Page web du projet : <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Nom de la version : %1$s</string>
|
||||
<string name="about_version_code">Code de version : %1$d</string>
|
||||
<string name="about_history">OpenTracks est une bifurcation de <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (abandonné en 2016).</string>
|
||||
@@ -30,10 +30,10 @@ limitations under the License.
|
||||
\n* Fonctionne complètement hors ligne
|
||||
\n* Affichage des traces sur une carte (voir ci-dessous)</string>
|
||||
<string name="help_track_map_title">Comment afficher un tracé sur une carte \?</string>
|
||||
<string name="help_track_map_content"> Utilisez la petite icône en forme de carte.
|
||||
\na) (Actualisation en direct) Installez <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Il fournit des cartes en ligne ou hors ligne grâce à <a href="https://www.openstreetmap.org ">OpenStreetMap</a>.
|
||||
\nOSMDashboard est en réalité une application séparée qui n\'accéder (temporairement) qu\'aux données des tracés qui vont être affichées.
|
||||
\nb) Via une application cartographique standard (p. ex. <a href="http://osmand.net/">OsmAnd</a> ou <a href="https://maps.me/">MAPS.ME</a> ). L’application doit prendre en charge le format de fichier KMZ (uniquement les lieux et les horodatages). Plus précisément, l\'application doit prendre en charge <i>ACTION_VIEW</i> avec le type MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||
<string name="help_track_map_content"> \u0020Utilisez la petite icône en forme de carte.
|
||||
\n \u0020 \u0020a) \u0020 \u0020(Actualisation en direct) Installez <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. \u0020 \u0020Il fournit des cartes en ligne ou hors ligne grâce à <a href="https://www.openstreetmap.org ">OpenStreetMap</a>.
|
||||
\n \u0020 \u0020OSMDashboard est en réalité une application séparée qui n\'accéder (temporairement) qu\'aux données des tracés qui vont être affichées. \u0020
|
||||
\n \u0020 \u0020b) \u0020 Via une application cartographique standard (p. ex. <a href="http://osmand.net/">OsmAnd</a> ou <a href="https://maps.me/">MAPS.ME</a> ). \u0020 L’application doit prendre en charge le format de fichier KMZ (uniquement les lieux et les horodatages). \u0020 Plus précisément, l\'application doit prendre en charge <i>ACTION_VIEW</i> avec le type MIME <i>application/vnd.google-earth.kmz</i>. \u0020</string>
|
||||
<string name="help_storage_pictures_title">Comment OpenTracks stocke-t-il les photos qui ont été prises en tant que repère \?</string>
|
||||
<string name="help_storage_pictures_content">Elles sont enregistrées dans le stockage interne de l\'application (c\'est-à-dire uniquement accessible dans OpenTracks).</string>
|
||||
<string name="help_track_export_title">Comment puis-je exporter/partager des tracés enregistrées \?</string>
|
||||
|
||||
@@ -175,7 +175,7 @@ limitations under the License.
|
||||
<string name="settings_chart_by_distance">Etter avstand</string>
|
||||
<string name="settings_chart_by_time">Etter tid</string>
|
||||
<string name="settings_recording_default_activity_title">Forvalgt aktivitetstype</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s mellom registrerte posisjoner</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s mellom posisjonskontroller</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Det blir opprettet et nytt segment hvis avstanden mellom to posisjoner er større enn %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Maks. avstand</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Avstandsintervall for opptak</string>
|
||||
@@ -479,7 +479,7 @@ limitations under the License.
|
||||
<string name="settings_sensors_title">Sensorer</string>
|
||||
<string name="settings_announcements_statistics_title">Statistikkmeldinger</string>
|
||||
<string name="settings_announcements_summary">Tid, distanse, talehastighet</string>
|
||||
<string name="settings_import_export_title">Import/eksport</string>
|
||||
<string name="settings_import_export_title">Import og eksport</string>
|
||||
<string name="settings_import_export_summary">Import, eksport, auto-eksport</string>
|
||||
<string name="settings_units_category">Enheter</string>
|
||||
<string name="settings_activity_defaults_category">Aktivitetens forvalg</string>
|
||||
@@ -589,4 +589,12 @@ limitations under the License.
|
||||
<string name="settings_announcements_lap_power">Runde-kraft</string>
|
||||
<string name="power_x_watt">Kraft {x, plural, =1 {1 watt} other {# watt} }</string>
|
||||
<string name="power">Kraft</string>
|
||||
<string name="settings_theme_switch_restart">Temaendringer kan kreve en manuell omstart.</string>
|
||||
<string name="value_internal_sensor">Intern sensor</string>
|
||||
<string name="sensor_state_pressure">Barometer</string>
|
||||
<string name="sensor_state_power_unit">W</string>
|
||||
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Filtrer Bluetooth-enheter</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">Vis alle Bluetooth-enheter som kan oppdages</string>
|
||||
<string name="permission_recording_failed">OpenTracks krever tillatelse for å få tilgang til posisjonen din samt enheter i nærheten.</string>
|
||||
</resources>
|
||||
@@ -609,4 +609,5 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="power">Power</string>
|
||||
<string name="settings_announcements_lap_power">Lap power</string>
|
||||
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watt} }</string>
|
||||
<string name="create_marker_error">Kan geen markering maken. Probeer het opnieuw.</string>
|
||||
</resources>
|
||||
@@ -30,7 +30,7 @@ limitations under the License.
|
||||
<string name="activity_type_driving_bus">prowadzenie autobusu</string>
|
||||
<string name="activity_type_driving_car">prowadzenie samochodu</string>
|
||||
<string name="activity_type_ferry">prom</string>
|
||||
<string name="activity_type_frisbee">fisbee</string>
|
||||
<string name="activity_type_frisbee">frizbee</string>
|
||||
<string name="activity_type_gliding">szybownictwo</string>
|
||||
<string name="activity_type_hang_gliding">lotniarstwo</string>
|
||||
<string name="activity_type_helicopter">helikopter</string>
|
||||
|
||||
@@ -151,7 +151,7 @@ limitations under the License.
|
||||
<string name="menu_aggregated_statistics">Toplu istatistikler</string>
|
||||
<string name="menu_delete">Sil</string>
|
||||
<string name="menu_edit">Düzenle</string>
|
||||
<string name="menu_export_all">Tümünü dışa aktar</string>
|
||||
<string name="menu_export_all">Tümünü dışa aktar (her parça başına bir dosya)</string>
|
||||
<string name="menu_help">Yardım</string>
|
||||
<string name="menu_import_all">Tümünü içe aktar</string>
|
||||
<string name="menu_insert_marker">İşaretçi ekle</string>
|
||||
@@ -330,7 +330,7 @@ limitations under the License.
|
||||
\n Aslında OSMDashboard, sadece gösterilecek olan parçaların verilerine (geçici olarak) erişebilen ayrı bir uygulamadır.
|
||||
\n b) Normal bir harita uygulaması aracılığıyla (ör. <a href=\"http://osmand.net/\">OsmAnd</a> ve <a href=\"https://maps.me/\">MAPS.ME< /a>). Bu uygulamaların KMZ dosya biçimini (yalnızca konumlar ve zaman damgaları) desteklemesi gerekir. Aslında, bir uygulamanın MIME <i>application/vnd.google-earth.kmz</i> ile <i>ACTION_VIEW</i> özelliğini desteklemesi gerekir. </i> "</string>
|
||||
<string name="help_storage_pictures_title">OpenTracks, işaretçi olarak çekilmiş resimleri nasıl saklar\?</string>
|
||||
<string name="about_url">Proje web sayfası: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_url">Proje ağ sayfası: <a href="%1$s">%1$s</a></string>
|
||||
<string name="help_track_export_title">Kayıtlı parkurları nasıl dışa aktarabilirim/paylaşabilirim\?</string>
|
||||
<string name="about_version_code">Sürüm kodu: %1$d</string>
|
||||
<string name="generic_choose_an_option">Bir seçenek belirleyin</string>
|
||||
@@ -343,7 +343,7 @@ limitations under the License.
|
||||
<string name="export_without_photos">fotoğrafsız</string>
|
||||
<string name="description_speed_source_gps">GPS</string>
|
||||
<string name="export_error_post_workout">Antrenman sonrası dışa aktarma başarısız oldu, lütfen dışa aktarma dizinini kontrol edin.</string>
|
||||
<string name="export_progress_summary_new_msg">Yeni parçalar dışa aktarıldı</string>
|
||||
<string name="export_progress_summary_new_msg">Yeni dosyalar dışa aktarıldı</string>
|
||||
<string name="generic_today">Bugün</string>
|
||||
<string name="generic_click_twice_cancel">İptal etmek için tekrar geri düğmesine tıklayın</string>
|
||||
<string name="generic_yesterday">Dün</string>
|
||||
@@ -415,9 +415,9 @@ limitations under the License.
|
||||
<string name="help_recording_content">OpenTracks, konumu devamlı olarak kaydetmek için cihazın GPS sensörünü kullanır. GPS cihazı yanlış veriler bildirirse (ör. konum, hız, yükseklik) OpenTracks bu konuda bir şey yapamaz.</string>
|
||||
<string name="help_data_storage_title">OpenTracks sensör verilerini nasıl depolar\?</string>
|
||||
<string name="export_dir_not_writable">" (yazılabilir değil!)"</string>
|
||||
<string name="export_track_errors">Parçalar dışa aktarılmadı</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Parçaların üzerine yazıldı</string>
|
||||
<string name="export_progress_summary_skip_msg">Parçalar atlandı</string>
|
||||
<string name="export_track_errors">Dosyalar dışa aktarılmadı</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Dosyaların üzerine yazıldı</string>
|
||||
<string name="export_progress_summary_skip_msg">Dosyalar atlandı</string>
|
||||
<string name="export_progress_summary_errors_msg">Hatalar</string>
|
||||
<string name="generic_open_track">Parçayı Aç</string>
|
||||
<string name="generic_overwrite">Üzerine yaz</string>
|
||||
@@ -435,7 +435,7 @@ limitations under the License.
|
||||
<string name="settings_ui_title">Kullanıcı Arayüzü</string>
|
||||
<string name="settings_default_trackfileformat">Dosya biçimini dışa aktarma/paylaşma</string>
|
||||
<string name="settings_export_filename_title">Dosya adının biçimi</string>
|
||||
<string name="settings_import_export_title">İçe/Dışa Aktar</string>
|
||||
<string name="settings_import_export_title">İçe ve Dışa Aktar</string>
|
||||
<string name="settings_import_export_summary">İçe, Dışa, Otomatik Dışa Aktar</string>
|
||||
<string name="settings_units_category">Birimler</string>
|
||||
<string name="settings_activity_defaults_category">Aktivitenin Varsayılanları</string>
|
||||
@@ -496,7 +496,7 @@ limitations under the License.
|
||||
<string name="instant_export_enabled_summary">Kayıt tamamlandıktan sonra parçayı depolamaya aktar</string>
|
||||
<string name="value_float_mile_hour">%1$.1f mi/s</string>
|
||||
<string name="settings_announcements_totaltime_frequency">Zaman aralığı</string>
|
||||
<string name="settings_announcements_distance_frequency">Mesafe aralığı</string>
|
||||
<string name="settings_announcements_distance_frequency">Tur uzunluğu</string>
|
||||
<string name="stats_sensors_heart_rate">Kalp atış hızı</string>
|
||||
<string name="stats_clock">Saat</string>
|
||||
<string name="stats_sensors_cadence">Kadans</string>
|
||||
@@ -544,4 +544,60 @@ limitations under the License.
|
||||
<string name="voice_per_nautical_mile">deniz mili başına</string>
|
||||
<string name="settings_public_api_package_title">Genel API için paket adı</string>
|
||||
<string name="show_on_dashboard_not_installed">OpenTracks Gösterge Paneli API\'sini destekleyen hiçbir uygulama yüklü değil.</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Bluetooth Cihazlarını Filtrele</string>
|
||||
<string name="settings_stats_units_imperial_meter">İngiliz Ölçü Birimi (mi, m)</string>
|
||||
<string name="settings_api_summary">Diğer uygulamalar için uzaktan kontrol ve istatistik Uygulama Programlama Arayüzü (API)</string>
|
||||
<string name="generic_from">\'den beri</string>
|
||||
<string name="image_discard">Çıkar</string>
|
||||
<string name="import_thread_interrupted">İzlek kesintiye uğradı</string>
|
||||
<string name="settings_theme_switch_restart">Tema değişiklikleri elle yeniden başlatmayı gerektirebilir.</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Kullanıcı tanımlı Dinamik Renkleri kullan (sadece Android 12+; elle yeniden başlatmayı gerektirir)</string>
|
||||
<string name="settings_night_mode_option_night_oled">Gece (OLED dostu)</string>
|
||||
<string name="settings_announcements_lap_heart_rate">Tur kalp atışı</string>
|
||||
<string name="settings_announcements_unit">Mesafe/hız birimi</string>
|
||||
<string name="settings_sensor_heart_rate_max">Alanlar: en yüksek Kalp atış hızı (atım/dk)</string>
|
||||
<string name="track_discarding">Çıkarılan Parçalar</string>
|
||||
<string name="unit_knots">kn</string>
|
||||
<string name="unit_nautical_mile">NM</string>
|
||||
<string name="value_internal_sensor">Dâhilî duyarga (sensör)</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {1 kilometre/saat} other {{n,number,#.0} kilometre/saat} }</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 mil/saat} other {{n,number,#.0} mil/saat} }</string>
|
||||
<string name="voice_on_device_speaker_title">Cihazın hoparlörünü kullan</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 not} other {{n,number,#.0} not} }</string>
|
||||
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {1 kilometre} other {{n,number,#.0} kilometre} }</string>
|
||||
<string name="voiceDistanceMilesPlural">{n, plural, =1 {1 mil} other {{n,number,#.0} mil} }</string>
|
||||
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 deniz mili} other {{n,number,#.0} deniz mili} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
|
||||
<string name="introduction_osm_dashboard">OpenTracks uygulaması harita işlevselliği sunmaz. Kayıtlarınızı bir haritada görüntülemek için lütfen OSMDashboard benzeri bir uygulama yükleyiniz.</string>
|
||||
<string name="settings_recording_min_sampling_interval_title">Örnekleme zaman aralığı</string>
|
||||
<string name="settings_recording_location_distance_summary">kayıtlı konumlar arasındaki mesafe: %1$s</string>
|
||||
<string name="no_compatible_camera_installed">Uyumlu kamera uygulaması yüklü değil</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">Tüm keşfedilebilir Bluetooth cihazlarını göster</string>
|
||||
<string name="sensor_state_cadence_unit">devir/dk</string>
|
||||
<string name="sensor_state_heart_rate_unit">atım/dk</string>
|
||||
<string name="voiceHoursPlural">{n, plural, =1 {1 saat} other {{n,number} saat} }</string>
|
||||
<string name="no_compatible_gallery_installed">Uyumlu galeri uygulaması yüklü değil</string>
|
||||
<string name="permission_recording_failed">OpenTracks, konumunuzun yanı sıra yakındaki cihazlara erişmek için de izin gerektirir.</string>
|
||||
<string name="generic_to">\'e</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Eğer hassasiyet oranı %1$s\'dan büyükse Konum göz ardı edilir</string>
|
||||
<string name="settings_recording_idle_timeout_title">Boşta Durma Eşiği</string>
|
||||
<string name="value_int_seconds">%1$d s (tavsiye edilen)</string>
|
||||
<string name="sensor_state_power_max">En Yüksek Güç</string>
|
||||
<string name="sensor_state_power_unit">W</string>
|
||||
<string name="settings_announcements_idle_category_title">Boşta Durma Bildirimleri</string>
|
||||
<string name="settings_announcements_idle_title">Boşta Durma</string>
|
||||
<string name="settings_recording_title">Kaydediliyor</string>
|
||||
<string name="settings_ui_dynamic_colors_title">Dinamik Renkler</string>
|
||||
<string name="voiceMinutesPlural">{n, plural, =1 {1 dakika} other {{n,number} dakika} }</string>
|
||||
<string name="voiceSecondsPlural">{n, plural, =1 {1 saniye} other {{n,number} saniye} }</string>
|
||||
<string name="voiceIdle">Boşta durma hâline geçiliyor.</string>
|
||||
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
|
||||
<string name="sensor_state_pressure">Basınçölçer</string>
|
||||
<string name="menu_export_all_one_file">Tümünü dışa aktar (tüm parçaları tek dosyada topla)</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d parçalar</string>
|
||||
<string name="settings_announcements_lap_power">Tur gücü</string>
|
||||
<string name="voice_announce_lap_power_key">voiceAnnounceLapPower</string>
|
||||
<string name="power_x_watt">Power {x, plural, =1 {1 vat(watt)} other {# vat(watt)} }</string>
|
||||
<string name="power">Güç</string>
|
||||
</resources>
|
||||
@@ -201,7 +201,6 @@ limitations under the License.
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="generic_show_errors">顯示錯誤</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">已完成,但有一個錯誤</item>
|
||||
<item quantity="other">已完成,但有 %1$d 個錯誤</item>
|
||||
</plurals>
|
||||
<string name="generic_completed">完成</string>
|
||||
|
||||
@@ -258,6 +258,7 @@ limitations under the License.
|
||||
<string name="import_progress_summary_exists_msg">Already exists</string>
|
||||
<string name="import_progress_summary_errors_msg">Errors</string>
|
||||
<!-- Marker -->
|
||||
<string name="create_marker_error">Unable to create a marker. Try again.</string>
|
||||
<string name="marker_add_error">Unable to insert a marker. No GPS signal. Try again.</string>
|
||||
<string name="marker_add_success">A marker was inserted</string>
|
||||
<string name="marker_add_canceled">Canceled, no marker added</string>
|
||||
|
||||
Reference in New Issue
Block a user