Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d90258294f | |||
| 01f4c13307 | |||
| 873956426d | |||
| 4128c54960 | |||
| 9978bddf21 | |||
| a9726b9a94 | |||
| 05ba52bab5 | |||
| ca95f40e77 | |||
| 5cb85c4383 | |||
| d0a477b112 | |||
| ec365a18ca | |||
| 0ff735e559 | |||
| 8a35835791 | |||
| e3569dab56 | |||
| 4040e3f45b | |||
| eac7efba64 | |||
| 21a3971a56 | |||
| 4e4a9abf6f | |||
| a9624f72e7 | |||
| bb38d274fb | |||
| 418f092db9 | |||
| fa12086951 | |||
| 4a340380ac | |||
| cd4e0abd6f | |||
| 854c8727e5 | |||
| 2f3d9a29f3 | |||
| 18cdbe94a9 | |||
| 18e179cfe3 | |||
| 63e3c7a4e7 | |||
| c6deca9bbd | |||
| 9ef6ae5e25 | |||
| 29b1ded68c | |||
| 6239e2fcad | |||
| bdfcd9c89f | |||
| f8f78d28dd | |||
| cbe469d5e4 | |||
| e0553f812f | |||
| 5aa016b5b9 | |||
| c3741611dc | |||
| d157e94220 | |||
| 022d8146fe | |||
| 912fd1aa2f | |||
| 1d46981717 | |||
| e990b413cd | |||
| 3bbcbfe202 | |||
| 9d8f49f1dc | |||
| 9f6d6711e5 | |||
| f635d56ceb | |||
| ab47320216 | |||
| 47b2d0748a | |||
| bc66cd33a9 | |||
| fcc17413e3 | |||
| 163af2dab2 | |||
| 393e2022a1 |
@@ -84,18 +84,16 @@ However, this is rather slow and does not provide updates while recording.
|
||||
|
||||
## Screenshots
|
||||
<div>
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot2.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot3.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot4.png">
|
||||
</div>
|
||||
<div>
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_7.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_8.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_9.png">
|
||||
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_10.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot5.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot6.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot7.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
|
||||
</div>
|
||||
|
||||
## Project history
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Folder where svg screenshots are expected.
|
||||
SRC_DIR="screenshots-svg/"
|
||||
# Folder where png files will be saved.
|
||||
DST_DIR="fastlane/metadata/android/en-US/phoneScreenshots/"
|
||||
|
||||
echo "Exporting svg files to png..."
|
||||
|
||||
# Source and destination folder must exist.
|
||||
if [ ! -d "$SRC_DIR" ]
|
||||
then
|
||||
echo "\nERROR: ${SRC_DIR} doesn't exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -d "$DST_DIR" ]
|
||||
then
|
||||
echo "\nERROR: ${DST_DIR} doesn't exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
# It needs Inkscape that is the tool used to convert svg to png.
|
||||
if ! command -v inkscape > /dev/null
|
||||
then
|
||||
echo "\nERROR: inkscape couldn't be found and it's needed to convert screenshots from svg to png."
|
||||
exit
|
||||
fi
|
||||
|
||||
# Convert all svg files from source directory.
|
||||
for path in ${SRC_DIR}/*.svg
|
||||
do
|
||||
filename="${path##*/}"
|
||||
basename="${filename%.svg}"
|
||||
`inkscape ${path} --export-filename ${DST_DIR}/${basename}.png`
|
||||
done
|
||||
@@ -51,8 +51,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 3821
|
||||
versionName "v3.14.2"
|
||||
versionCode 3875
|
||||
versionName "v3.15.2"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -86,18 +86,18 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha03'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.7.7'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ class Track {
|
||||
}
|
||||
Track o-- TrackPoint
|
||||
Track o-- Marker
|
||||
Track "1" - "1" TripStatistics
|
||||
Track "1" - "1" TrackStatistics
|
||||
|
||||
class TripStatistics {
|
||||
class TrackStatistics {
|
||||
- startTime
|
||||
- stopTime
|
||||
- totalDistance
|
||||
@@ -21,13 +21,15 @@ class TripStatistics {
|
||||
- totalElevationGain
|
||||
}
|
||||
|
||||
note left of TripStatistics
|
||||
The time is based upon device time.
|
||||
end note
|
||||
class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
TrackStatisticsUpdater ..> TrackStatistics : creates/updates
|
||||
TrackStatisticsUpdater ..> TrackPoint : uses
|
||||
|
||||
class TrackPoint {
|
||||
- id
|
||||
- id (database id, order)
|
||||
- type
|
||||
- trackId
|
||||
- longitude
|
||||
- latitude
|
||||
@@ -41,30 +43,6 @@ class TrackPoint {
|
||||
- sensor_power
|
||||
}
|
||||
|
||||
note right of TrackPoint
|
||||
TODO
|
||||
TrackPoints are mixing two concepts:
|
||||
1. a GPS measurement provided by the GPS hardware
|
||||
2. and also segment markers.
|
||||
These are marked by PAUSE / RESUME TrackPoints.
|
||||
|
||||
While exporting PAUSE/RESUME are not used to create these segments and these are partly recreated when imported.
|
||||
However, the timestamp of PAUSE/RESUME is not exported and can therefore not be restored.
|
||||
This prevents us from restoring the exact TripStatistics.
|
||||
It is also noteworthy that neither PAUSE/RESUME TrackPoints are used for computing the TrackStatistics.
|
||||
Only the timestamp to _create_ those is used.
|
||||
end note
|
||||
|
||||
note left of TrackPoint
|
||||
TODO
|
||||
The time stored in TrackPoints is either:
|
||||
1. created via GPS: provided by the GPS hardware
|
||||
2. created by the user (pause/resume): device time.
|
||||
|
||||
We know that the GPS time and device time are often not in sync.
|
||||
end note
|
||||
|
||||
TrackPoint "1" - "1" android.location.Location
|
||||
|
||||
class Marker {
|
||||
- id
|
||||
@@ -75,8 +53,28 @@ class Marker {
|
||||
- icon
|
||||
- length
|
||||
- duration
|
||||
- location
|
||||
- tripStatistics
|
||||
- longitude
|
||||
- latitude
|
||||
- photoUrl
|
||||
}
|
||||
|
||||
note left of TrackPoint
|
||||
As of OpenTracks version 3.15.0, all times are using device time.
|
||||
Before that TrackPoint.time contained GPS time (determined by GPS hardware).
|
||||
However, start/pause/stop events (also stored as TrackPoints) used device time.
|
||||
end note
|
||||
|
||||
note right of Track
|
||||
A track is an ordered collection of one or more segments (i.e., continuous parts of distance covered).
|
||||
Segments may be started by a user (i.e., start a track recording, continue a paused track, or resume a track) as well as stopped by the user (i.e., pausing or stopping a recording).
|
||||
Also segments may started automatically while recording (i.e., distance to previous location was to large).
|
||||
Note that this finishes the previous segment.
|
||||
|
||||
Segment data is stored as TrackPoints (Type.SEGMENT_START_MANUAL, Type.SEGMENT_START_AUTOMATIC, or Type.SEGMENT_END_MANUAL).
|
||||
Trackpoints with Type.SEGMENT_START_AUTOMATIC also mark the end of the previous segment.
|
||||
All TrackPoints of Type.TRACKPOINT belong to the segment started by the prior TrackPoint with Type.SEGMENT_START_(MANUAL|AUTOMATIC).
|
||||
Trackpoints of Type.SEGMENT_START_MANUAL or Type.SEGMENT_END_MANUAL do not contain location data or sensor data.
|
||||
|
||||
Tracks recorded prior to OpenTracks version 3.15.0, do neither begin with a Type.SEGMENT_START_MANUAL nor end with a Type.SEGMENT_END_MANUAL.
|
||||
end note
|
||||
@enduml
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.14.3: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Downgrading database from upcoming release v3.15.0
|
||||
@@ -0,0 +1,22 @@
|
||||
v3.15.0: OpenTracks
|
||||
|
||||
WARNING:
|
||||
This release applies database changes that CANNOT BE REVERTED.
|
||||
As KMZ/KML export/import behaviour changes as well, it is recommended to (A) take a backup BEFORE upgrading and (B) afterwards.
|
||||
Recommended format: KMZ.
|
||||
|
||||
BREAKING CHANGES:
|
||||
- Internal storage of track segments is changed
|
||||
(user generated events are now considered trackpoints without a location but with timestamp)
|
||||
- Device time instead of GPS time is stored
|
||||
(existing data is not modified)
|
||||
- Dashboard API v2
|
||||
(please upgrade OSMDashboard 2.10.0)
|
||||
|
||||
Changes:
|
||||
- KML/KMZ can used to backup and restore exactly (incl. statistics)
|
||||
|
||||
Developer:
|
||||
- Reworked track segmentation and it's storage (formerly PAUSE/RESUME marker)
|
||||
- TrackStatistics are computed solely using TrackPoints
|
||||
- Replaced long with Instant/Duration
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.15.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- After recording show sensor statistics (e.g., avg HR)
|
||||
|
||||
Bugfix:
|
||||
- Crash on recording start with show latitude/longitude enabled
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.15.2: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Updated translations
|
||||
|
||||
Bugfix:
|
||||
- Crash on GPX export
|
||||
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 508 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 862 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 665 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 661 KiB |
|
After Width: | Height: | Size: 1001 KiB |
@@ -25,6 +25,9 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@@ -40,7 +43,7 @@ import static org.junit.Assert.assertEquals;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DescriptionGeneratorTest {
|
||||
|
||||
private static final long START_TIME = 1288721514000L;
|
||||
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
|
||||
private DescriptionGenerator descriptionGenerator;
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
@@ -58,14 +61,14 @@ public class DescriptionGeneratorTest {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(600000);
|
||||
stats.setMovingTime(300000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setMaxElevation(550);
|
||||
stats.setMinElevation(-500);
|
||||
stats.setTotalElevationGain(6000);
|
||||
stats.setTotalElevationLoss(6000);
|
||||
stats.setStartTime_ms(START_TIME);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
stats.setTotalElevationLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setCategory("hiking");
|
||||
String expected = //"Created by"
|
||||
@@ -96,14 +99,14 @@ public class DescriptionGeneratorTest {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(600000);
|
||||
stats.setMovingTime(300000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setMaxElevation(Double.POSITIVE_INFINITY);
|
||||
stats.setMinElevation(Double.NEGATIVE_INFINITY);
|
||||
stats.setTotalElevationGain(6000);
|
||||
stats.setTotalElevationLoss(6000);
|
||||
stats.setStartTime_ms(START_TIME);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
stats.setTotalElevationLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setCategory("hiking");
|
||||
String expected = //"Created by"
|
||||
@@ -139,12 +142,12 @@ public class DescriptionGeneratorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeTime(long, StringBuilder, int, String)}.
|
||||
* Tests {@link DescriptionGenerator#writeTime(Duration, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteTime() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>");
|
||||
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time, "<br>");
|
||||
assertEquals("Total time: 00:01<br>", builder.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -38,6 +38,7 @@ public class TestDataUtil {
|
||||
* @param trackId the trackId of the track
|
||||
* @param numPoints the trackPoints number in the track
|
||||
*/
|
||||
@Deprecated //TODO Should start with SEGMENT_START_MANUAL and end with SEGMENT_END_MANUAL.
|
||||
public static Pair<Track, List<TrackPoint>> createTrack(Track.Id trackId, int numPoints) {
|
||||
Track track = createTrack(trackId);
|
||||
|
||||
@@ -49,6 +50,51 @@ public class TestDataUtil {
|
||||
return new Pair<>(track, trackPoints);
|
||||
}
|
||||
|
||||
|
||||
public static TrackData createTestingTrack(Track.Id trackId) {
|
||||
Track track = createTrack(trackId);
|
||||
|
||||
int i = 0;
|
||||
List<TrackPoint> trackPoints = List.of(
|
||||
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochMilli(i++ + 1)),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++, TrackPoint.Type.SEGMENT_START_AUTOMATIC),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1)),
|
||||
|
||||
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1))
|
||||
);
|
||||
|
||||
List<Marker> markers = List.of(
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(5), null)
|
||||
);
|
||||
|
||||
return new TrackData(track, trackPoints, markers);
|
||||
}
|
||||
|
||||
public static class TrackData {
|
||||
public final Track track;
|
||||
public final List<TrackPoint> trackPoints;
|
||||
public final List<Marker> markers;
|
||||
|
||||
public TrackData(Track track, List<TrackPoint> trackPoints, List<Marker> markers) {
|
||||
this.track = track;
|
||||
this.trackPoints = trackPoints;
|
||||
this.markers = markers;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) {
|
||||
Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints);
|
||||
|
||||
@@ -63,12 +109,12 @@ public class TestDataUtil {
|
||||
* @param i the index for the TrackPoint.
|
||||
*/
|
||||
public static TrackPoint createTrackPoint(int i) {
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
|
||||
trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
|
||||
trackPoint.setAccuracy((float) i / 100.0f);
|
||||
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
|
||||
trackPoint.setTime(i + 1);
|
||||
trackPoint.setTime(Instant.ofEpochSecond(i + 1));
|
||||
trackPoint.setSpeed(5f + (i / 10f));
|
||||
|
||||
trackPoint.setHeartRate_bpm(100f + i);
|
||||
@@ -79,6 +125,12 @@ public class TestDataUtil {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
|
||||
TrackPoint trackPoint = createTrackPoint(i);
|
||||
trackPoint.setType(type);
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track with locations into the database.
|
||||
*
|
||||
@@ -90,20 +142,12 @@ public class TestDataUtil {
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackPoints, track.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Marker with a photo.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param trackId The track id.
|
||||
* @param location The location.
|
||||
* @return the Marker created.
|
||||
*/
|
||||
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
|
||||
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, TrackPoint trackPoint) throws IOException {
|
||||
File dstFile = new File(FileUtils.getImageUrl(context, trackId));
|
||||
dstFile.createNewFile();
|
||||
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
|
||||
String photoUrl = photoUri.toString();
|
||||
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, location, photoUrl);
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, trackPoint, photoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -231,7 +233,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
contentProviderUtils.insertMarker(marker);
|
||||
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
@@ -270,7 +272,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track.Id trackId2 = new Track.Id(random + 1);
|
||||
Track.Id trackId3 = new Track.Id(random + 2);
|
||||
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 0);
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 10);
|
||||
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
|
||||
@@ -282,7 +284,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
|
||||
assertEquals(3, tracksCursor.getCount());
|
||||
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
|
||||
assertEquals(20, tracksPointsCursor.getCount());
|
||||
assertEquals(30, tracksPointsCursor.getCount());
|
||||
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
|
||||
assertEquals(1, markerCursor.getCount());
|
||||
// Delete one track.
|
||||
@@ -313,12 +315,12 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
// Insert a marker in tracks trackId and trackId + 1.
|
||||
TrackPoint trackPoint1 = contentProviderUtils.getLastValidTrackPoint(trackId1);
|
||||
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId1, trackPoint1.getLocation());
|
||||
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId1, trackPoint1);
|
||||
contentProviderUtils.insertMarker(marker1);
|
||||
File dir1 = FileUtils.getPhotoDir(context, trackId1);
|
||||
|
||||
TrackPoint trackPoint2 = contentProviderUtils.getLastValidTrackPoint(trackId2);
|
||||
Marker marker2 = TestDataUtil.createMarkerWithPhoto(context, trackId2, trackPoint2.getLocation());
|
||||
Marker marker2 = TestDataUtil.createMarkerWithPhoto(context, trackId2, trackPoint2);
|
||||
contentProviderUtils.insertMarker(marker2);
|
||||
File dir2 = FileUtils.getPhotoDir(context, trackId2);
|
||||
|
||||
@@ -423,14 +425,12 @@ public class CustomContentProviderUtilsTest {
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
|
||||
|
||||
// Bottom
|
||||
long startTime = 1000L;
|
||||
// AverageSpeed
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setStartTime_ms(startTime);
|
||||
statistics.setStopTime_ms(2500L);
|
||||
statistics.setTotalTime(1500L);
|
||||
statistics.setMovingTime(700L);
|
||||
statistics.setStartTime(Instant.ofEpochMilli(1000));
|
||||
statistics.setStopTime(Instant.ofEpochMilli(2500));
|
||||
statistics.setTotalTime(Duration.ofMillis(1500));
|
||||
statistics.setMovingTime(Duration.ofMillis(700));
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(60.0);
|
||||
@@ -527,7 +527,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
// Insert at first.
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
contentProviderUtils.insertMarker(marker1);
|
||||
|
||||
// Check insert was done.
|
||||
@@ -661,7 +661,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
// Insert at first.
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
marker.setDescription(TEST_DESC);
|
||||
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
|
||||
|
||||
@@ -696,7 +696,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
// Insert at first.
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
marker.setDescription(TEST_DESC);
|
||||
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
|
||||
|
||||
@@ -730,9 +730,9 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
// Insert two markers with photos.
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
marker.setDescription(TEST_DESC);
|
||||
Marker otherMarker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
Marker otherMarker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
|
||||
otherMarker.setDescription(TEST_DESC);
|
||||
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
|
||||
contentProviderUtils.insertMarker(otherMarker);
|
||||
@@ -809,7 +809,7 @@ public class CustomContentProviderUtilsTest {
|
||||
// then
|
||||
assertEquals(longitude, trackPoint.getLongitude(), 0.01);
|
||||
assertEquals(latitude, trackPoint.getLatitude(), 0.01);
|
||||
assertEquals(time, trackPoint.getTime(), 0.01);
|
||||
assertEquals(time, trackPoint.getTime().toEpochMilli());
|
||||
assertEquals(speed, trackPoint.getSpeed(), 0.01);
|
||||
assertFalse(trackPoint.hasHeartRate());
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -37,7 +37,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(33.53, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(33.53, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -50,7 +50,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(0, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(0, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertFalse(current.hasCadence_rpm());
|
||||
assertFalse(current.hasValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,7 +77,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,7 +90,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getCadence_rpm(), 0.01);
|
||||
assertEquals(60, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,7 +103,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous, 2150);
|
||||
|
||||
// then
|
||||
assertEquals(1.20, current.getSpeed_mps(), 0.01);
|
||||
assertEquals(1.20, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,7 +116,7 @@ public class SensorDataCyclingTest {
|
||||
current.compute(previous, 2000);
|
||||
|
||||
// then
|
||||
assertEquals(2, current.getSpeed_mps(), 0.01);
|
||||
assertEquals(2, current.getValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -28,6 +28,9 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -61,9 +64,8 @@ public class ChartFragmentTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
boolean chartByDistance = false;
|
||||
chartFragment = (ChartFragment) ChartFragment.newInstance(chartByDistance);
|
||||
chartFragment = ChartFragment.newInstance(chartByDistance);
|
||||
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
|
||||
chartFragment.setTrackStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,15 +193,21 @@ public class ChartFragmentTest {
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_time() {
|
||||
// given
|
||||
chartFragment.setChartByDistance(false);
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint1.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME)); //Keep old TrackPoint behavior of having time=0 for this test
|
||||
|
||||
// when
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
|
||||
// then
|
||||
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
|
||||
long timeSpan = 222;
|
||||
Duration timeSpan = Duration.ofMillis(222);
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint2.setTime(trackPoint1.getTime() + timeSpan);
|
||||
trackPoint2.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME).plus(timeSpan));
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
|
||||
assertEquals(timeSpan, Duration.ofMillis((long) point.getTimeOrDistance()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,8 +258,8 @@ public class ChartFragmentTest {
|
||||
/*
|
||||
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint1.getTime() + 222);
|
||||
trackPoint2.setSpeed(130);
|
||||
trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(130f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
|
||||
}
|
||||
@@ -275,8 +283,8 @@ public class ChartFragmentTest {
|
||||
* Add a time span here to make sure the second point and the speed is valid.
|
||||
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint2.getTime() + 222);
|
||||
trackPoint2.setSpeed(102);
|
||||
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(102f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
|
||||
}
|
||||
@@ -300,8 +308,8 @@ public class ChartFragmentTest {
|
||||
* Add a time span here to make sure the second point and the speed is valid.
|
||||
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint2.getTime() + 222);
|
||||
trackPoint2.setSpeed(102);
|
||||
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(102f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
|
||||
}
|
||||
@@ -313,7 +321,7 @@ public class ChartFragmentTest {
|
||||
public void testCreatePendingPoint_pace_zeroSpeed() {
|
||||
chartFragment.setReportSpeed(false);
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint.setSpeed(0);
|
||||
trackPoint.setSpeed(0f);
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
|
||||
assertEquals(0.0, point.getPace(), 0.01);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class TrackStubUtils {
|
||||
location.setAltitude(altitude);
|
||||
location.setAccuracy(INITIAL_ACCURACY);
|
||||
location.setSpeed(INITIAL_SPEED);
|
||||
location.setTime(INITIAL_TIME);
|
||||
location.setTime(INITIAL_TIME); //TODO This is nowadays ignored as the constructor will replace the time.
|
||||
location.setBearing(INITIAL_BEARING);
|
||||
|
||||
return new TrackPoint(location);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class KmlTrackExporterTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
* Sensor data by type should only be created if present in at least on TrackPoint.
|
||||
*/
|
||||
@Test
|
||||
public void writeCloseSegment_only_write_sensordata_if_present() {
|
||||
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<ExtendedData>\n" +
|
||||
"<SchemaData schemaUrl=\"#schema\">\n" +
|
||||
"</SchemaData>\n" +
|
||||
"</ExtendedData>\n" +
|
||||
"</gx:Track>\n";
|
||||
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
kmlTrackWriter.prepare(outputStream);
|
||||
|
||||
kmlTrackWriter.writeTrackPoint(trackPoint);
|
||||
|
||||
// when
|
||||
kmlTrackWriter.writeCloseSegment();
|
||||
kmlTrackWriter.close();
|
||||
|
||||
// then
|
||||
assertEquals(expected, outputStream.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
@@ -19,32 +25,49 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
|
||||
* <p>
|
||||
* TODO: test ignores {@link TrackStatistics} for now.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportImportTest {
|
||||
|
||||
private static final String TAG = ExportImportTest.class.getSimpleName();
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -53,37 +76,43 @@ public class ExportImportTest {
|
||||
private static final String TRACK_CATEGORY = "the category";
|
||||
private static final String TRACK_DESCRIPTION = "the description";
|
||||
|
||||
private final List<Marker> markers = new ArrayList<>();
|
||||
private final List<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private Track track;
|
||||
private List<Marker> markers = new ArrayList<>();
|
||||
private List<TrackPoint> trackPoints = new ArrayList<>();
|
||||
|
||||
private Track.Id trackId;
|
||||
private Track.Id importTrackId;
|
||||
private final Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
|
||||
track.first.setIcon(TRACK_ICON);
|
||||
track.first.setCategory(TRACK_CATEGORY);
|
||||
track.first.setDescription(TRACK_DESCRIPTION);
|
||||
contentProviderUtils.insertTrack(track.first);
|
||||
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
|
||||
public void setUp() throws TimeoutException {
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)));
|
||||
|
||||
trackPoints.clear();
|
||||
trackPoints.addAll(track.second);
|
||||
trackId = service.startNewTrack();
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Marker marker = new Marker(trackId, track.second.get(i).getLocation());
|
||||
marker.setName("the marker " + i);
|
||||
marker.setDescription("the marker description " + i);
|
||||
marker.setCategory("the marker category" + i);
|
||||
marker.setIcon("the marker icon" + i);
|
||||
marker.setPhotoUrl("the photo url" + i);
|
||||
contentProviderUtils.insertMarker(marker);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
service.pauseCurrentTrack();
|
||||
|
||||
markers.add(marker);
|
||||
}
|
||||
service.resumeCurrentTrack();
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50), 0);
|
||||
service.endCurrentTrack();
|
||||
|
||||
assertEquals(markers.size(), contentProviderUtils.getMarkerCount(trackId));
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
track.setIcon(TRACK_ICON);
|
||||
track.setCategory(TRACK_CATEGORY);
|
||||
track.setDescription(TRACK_DESCRIPTION);
|
||||
contentProviderUtils.updateTrack(track);
|
||||
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
markers = contentProviderUtils.getMarkers(trackId);
|
||||
assertEquals(10, trackPoints.size());
|
||||
assertEquals(2, markers.size());
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -94,6 +123,7 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Not implemented")
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_only_track() {
|
||||
@@ -107,7 +137,7 @@ public class ExportImportTest {
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL.newTrackExporter(context);
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
@@ -130,11 +160,14 @@ public class ExportImportTest {
|
||||
assertEquals(track.getIcon(), importedTrack.getIcon());
|
||||
assertEquals(track.getUuid(), importedTrack.getUuid());
|
||||
|
||||
// 2. markers
|
||||
assertMarkers();
|
||||
// 2. trackpoints
|
||||
assertTrackpoints(trackPoints, false, false, false, false, false);
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(false, false, false, false, false);
|
||||
// 3. trackstatistics
|
||||
assertTrackStatistics(false, false);
|
||||
|
||||
// 4. markers
|
||||
assertMarkers();
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@@ -143,7 +176,7 @@ public class ExportImportTest {
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context);
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
@@ -165,11 +198,14 @@ public class ExportImportTest {
|
||||
assertEquals(track.getName(), importedTrack.getName());
|
||||
assertEquals(track.getIcon(), importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertMarkers();
|
||||
// 2. trackpoints
|
||||
assertTrackpoints(trackPoints, true, true, true, true, true);
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(true, true, true, true, true);
|
||||
// 2. trackstatistics
|
||||
assertTrackStatistics(false, true);
|
||||
|
||||
// 4. markers
|
||||
assertMarkers();
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@@ -179,7 +215,7 @@ public class ExportImportTest {
|
||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context);
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
@@ -234,7 +270,7 @@ public class ExportImportTest {
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context);
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
@@ -258,11 +294,20 @@ public class ExportImportTest {
|
||||
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
|
||||
//assertEquals(track.getIcon(), trackImported.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertMarkers();
|
||||
// 2. trackpoints
|
||||
// The GPX exporter does not support exporting TrackPoints without lat/lng.
|
||||
// Therefore, the track segmentation is changes.
|
||||
List<TrackPoint> trackPointsWithCoordinates = trackPoints.stream().filter(it -> TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(it.getType()) || TrackPoint.Type.TRACKPOINT.equals(it.getType())).collect(Collectors.toList());
|
||||
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(false, true, true, true, true);
|
||||
assertTrackpoints(trackPointsWithCoordinates, false, true, true, true, true);
|
||||
|
||||
// 3. trackstatistics
|
||||
assertTrackStatistics(true, true);
|
||||
|
||||
// 4. markers
|
||||
assertMarkers();
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@@ -272,7 +317,7 @@ public class ExportImportTest {
|
||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context);
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
@@ -309,23 +354,42 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
|
||||
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
|
||||
assertEquals(trackPoints.size(), importedTrackPoints.size());
|
||||
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
TrackPoint importedTrackPoint = importedTrackPoints.get(i);
|
||||
|
||||
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01);
|
||||
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime());
|
||||
TrackPoint.Type type = trackPoint.getType();
|
||||
Log.e(TAG, "" + importedTrackPoint.getType().equals(type));
|
||||
assertEquals("" + i, type, importedTrackPoint.getType());
|
||||
|
||||
// TODO Not exported for GPX/KML
|
||||
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
||||
|
||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
|
||||
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
|
||||
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
if (trackPoint.hasLocation()) {
|
||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
if (trackPoint.hasSpeed()) {
|
||||
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||
if (trackPoint.hasAltitude()) {
|
||||
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
|
||||
}
|
||||
|
||||
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
|
||||
//TODO REMOVE
|
||||
continue;
|
||||
}
|
||||
|
||||
if (verifyHeartrate) {
|
||||
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
}
|
||||
@@ -343,4 +407,59 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss) {
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
|
||||
assertNotNull(importedTrack.getTrackStatistics());
|
||||
|
||||
TrackStatistics trackStatistics = track.getTrackStatistics();
|
||||
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
|
||||
|
||||
// Time
|
||||
assertTrue(trackStatistics.getStartTime().isBefore(trackStatistics.getStopTime())); //Just to be sure.
|
||||
if (!isGpx) {
|
||||
assertEquals(trackStatistics.getStartTime(), importedTrackStatistics.getStartTime());
|
||||
assertEquals(trackStatistics.getStopTime(), importedTrackStatistics.getStopTime());
|
||||
|
||||
assertEquals(trackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
|
||||
assertEquals(trackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
|
||||
|
||||
// Distance
|
||||
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
|
||||
|
||||
// Speed
|
||||
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
|
||||
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed(), 0.01);
|
||||
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed(), 0.01);
|
||||
}
|
||||
|
||||
// Elevation
|
||||
assertEquals(trackStatistics.getMinElevation(), importedTrackStatistics.getMinElevation(), 0.01);
|
||||
assertEquals(trackStatistics.getMaxElevation(), importedTrackStatistics.getMaxElevation(), 0.01);
|
||||
if (verifyElevationGainAndLoss) {
|
||||
assertEquals(trackStatistics.getTotalElevationGain(), importedTrackStatistics.getTotalElevationGain(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalElevationLoss(), importedTrackStatistics.getTotalElevationLoss(), 0.01);
|
||||
} else {
|
||||
assertFalse(importedTrackStatistics.hasTotalElevationGain());
|
||||
assertFalse(importedTrackStatistics.hasTotalElevationLoss());
|
||||
}
|
||||
}
|
||||
|
||||
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, long speed, long altitude, float elevationGain, float heartRate, float cyclingCadence, float power) {
|
||||
Location location = new Location("");
|
||||
location.setTime(time);
|
||||
location.setLongitude(longitude);
|
||||
location.setLatitude(latitude);
|
||||
location.setAccuracy(accuracy);
|
||||
location.setAltitude(altitude);
|
||||
location.setSpeed(speed);
|
||||
|
||||
TrackPoint tp = new TrackPoint(location);
|
||||
tp.setHeartRate_bpm(heartRate);
|
||||
tp.setCyclingCadence_rpm(cyclingCadence);
|
||||
tp.setPower(power);
|
||||
tp.setElevationGain(elevationGain);
|
||||
return tp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data with statistics markers is imported and those ignored.
|
||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_with_statistics_marker() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
/**
|
||||
* At least one valid location is required.
|
||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_without_locations() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
|
||||
|
||||
// when
|
||||
importTrackId = trackImporter.importFile(inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void gpx_with_pause_resume() {
|
||||
// given
|
||||
GpxFileTrackImporter trackImporter = new GpxFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("the category", importedTrack.getCategory());
|
||||
assertEquals("the description", importedTrack.getDescription());
|
||||
assertEquals("2021-01-07 22:51", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first segment
|
||||
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2021-01-07T21:51:59.179Z", 14.0, 3.0, 10.0);
|
||||
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:00.653Z", 14.001, 3.0, 10.0);
|
||||
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:01.010Z", 14.002, 3.0, 10.0);
|
||||
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-01-07T21:52:02.658Z", null, null, null);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-01-07T21:52:03.873Z", null, null, null);
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
|
||||
}
|
||||
|
||||
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||
assertEquals(type, trackPoint.getType());
|
||||
|
||||
if (longitude == null) {
|
||||
assertFalse(trackPoint.hasLocation());
|
||||
} else {
|
||||
assertEquals(latitude, (Double) trackPoint.getLatitude());
|
||||
assertEquals(longitude, (Double) trackPoint.getLongitude());
|
||||
}
|
||||
|
||||
if (altitude == null) {
|
||||
assertFalse(trackPoint.hasAltitude());
|
||||
} else {
|
||||
assertEquals(altitude, (Double) trackPoint.getAltitude());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Imports a legacy kml file with a statistics marker at the beginning
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyKmlImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_legacy_track_import() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_track);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + pause and resume
|
||||
|
||||
// first 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created pause trackpoint with time of previous valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 100, 0.0);
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 200, 0.0);
|
||||
|
||||
// last 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
|
||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||
assertEquals(latitude, trackPoint.getLatitude(), 0.001d);
|
||||
assertEquals(longitude, trackPoint.getLongitude(), 0.001d);
|
||||
assertEquals(altitude, trackPoint.getAltitude(), 0.001d);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.services.handlers.AdaptiveLocationListenerPolicy;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -32,13 +34,13 @@ import static org.junit.Assert.assertEquals;
|
||||
@RunWith(JUnit4.class)
|
||||
public class AdaptiveLocationListenerPolicyTest {
|
||||
|
||||
private static final long MIN = 1000;
|
||||
private static final long MAX = 3000;
|
||||
private static final Duration MIN = Duration.ofMillis(1000);
|
||||
private static final Duration MAX = Duration.ofMillis(3000);
|
||||
private static final int MIN_DISTANCE = 10;
|
||||
private static final long NEW_IDLE_TIME_BIG = 10000;
|
||||
private static final long NEW_IDLE_TIME_NORMAL = 5000;
|
||||
private static final long NEW_IDLE_TIME_SMALL = 2000;
|
||||
private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500;
|
||||
private static final Duration NEW_IDLE_TIME_BIG = Duration.ofMillis(10000);
|
||||
private static final Duration NEW_IDLE_TIME_NORMAL = Duration.ofMillis(5000);
|
||||
private static final Duration NEW_IDLE_TIME_SMALL = Duration.ofMillis(2000);
|
||||
private static final Duration NEW_IDLE_TIME_LESS_THAN_MIN = Duration.ofMillis(500);
|
||||
private AdaptiveLocationListenerPolicy adaptiveLocationListenerPolicy;
|
||||
|
||||
@Before
|
||||
@@ -62,7 +64,7 @@ public class AdaptiveLocationListenerPolicyTest {
|
||||
|
||||
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
|
||||
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
|
||||
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
|
||||
assertEquals(NEW_IDLE_TIME_NORMAL.dividedBy(2).getSeconds(), adaptiveLocationListenerPolicy.getDesiredPollingInterval().getSeconds());
|
||||
|
||||
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
|
||||
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
|
||||
|
||||
@@ -47,7 +47,6 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@@ -158,7 +157,7 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_oldTracks() throws TimeoutException {
|
||||
// given
|
||||
createDummyTrack(trackId, -1L, false);
|
||||
createDummyTrack(trackId, false);
|
||||
|
||||
// when
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
@@ -172,7 +171,7 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
|
||||
// given
|
||||
createDummyTrack(trackId, -1L, true);
|
||||
createDummyTrack(trackId, true);
|
||||
|
||||
//when
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
@@ -183,11 +182,44 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_pauseAndResume() throws TimeoutException, InterruptedException {
|
||||
public void testRecording_start() throws TimeoutException {
|
||||
// given
|
||||
createDummyTrack(trackId, -1L, true);
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
newTrackPoint(service);
|
||||
|
||||
// when
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// then
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
|
||||
assertEquals(1, trackPoints.size());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_stop() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// when
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
|
||||
assertEquals(2, trackPoints.size());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_pauseAndResume() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// when
|
||||
service.pauseCurrentTrack();
|
||||
@@ -197,29 +229,28 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
//when
|
||||
service.resumeTrack(trackId);
|
||||
newTrackPoint(service);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(trackId, service.getRecordingTrackId());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(5, trackPoints.size());
|
||||
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
|
||||
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
|
||||
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
|
||||
assertEquals(3, trackPoints.size());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
|
||||
// given
|
||||
createDummyTrack(trackId, -1L, true);
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
newTrackPoint(service);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
service.endCurrentTrack();
|
||||
|
||||
assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
|
||||
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
|
||||
|
||||
// when
|
||||
service.resumeTrack(trackId);
|
||||
@@ -231,8 +262,34 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(4, trackPoints.size());
|
||||
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
|
||||
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
|
||||
assertEquals(TrackPoint.Type.TRACKPOINT, trackPoints.get(3).getType());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_stopPausedTrack() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
service.pauseCurrentTrack();
|
||||
|
||||
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
|
||||
|
||||
// when
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(2, trackPoints.size());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
|
||||
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
|
||||
}
|
||||
|
||||
@Ignore("Sometimes fails on CI.")
|
||||
@@ -336,12 +393,11 @@ public class TrackRecordingServiceTest {
|
||||
}
|
||||
|
||||
// NOTE: Do not use to create a track that is currently recording.
|
||||
private void createDummyTrack(Track.Id id, long stopTime, boolean isRecording) {
|
||||
private void createDummyTrack(Track.Id id, boolean isRecording) {
|
||||
Track dummyTrack = new Track();
|
||||
dummyTrack.setId(id);
|
||||
dummyTrack.setName("Dummy Track");
|
||||
TrackStatistics trackStatistics = new TrackStatistics();
|
||||
trackStatistics.setStopTime_ms(stopTime);
|
||||
dummyTrack.setTrackStatistics(trackStatistics);
|
||||
addTrack(dummyTrack, isRecording);
|
||||
}
|
||||
@@ -358,7 +414,7 @@ public class TrackRecordingServiceTest {
|
||||
* Inserts a location and waits for 200ms.
|
||||
*/
|
||||
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
|
||||
Location location = new Location("gps");
|
||||
Location location = new Location("");
|
||||
location.setLongitude(longitude);
|
||||
location.setLatitude(latitude);
|
||||
location.setAccuracy(accuracy);
|
||||
@@ -368,8 +424,5 @@ public class TrackRecordingServiceTest {
|
||||
TrackPoint trackPoint = new TrackPoint(location);
|
||||
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(ApplicationProvider.getApplicationContext());
|
||||
trackRecordingService.newTrackPoint(trackPoint, prefAccuracy);
|
||||
|
||||
//TODO Needed?
|
||||
Thread.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.ContentProvider;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Looper;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@@ -24,7 +25,6 @@ import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
@@ -34,6 +34,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Tests insert location.
|
||||
@@ -69,6 +70,7 @@ public class TrackRecordingServiceTestLocation {
|
||||
customContentProvider.attachInfo(context, null);
|
||||
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
tearDown();
|
||||
|
||||
// Let's use default values.
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
@@ -79,11 +81,6 @@ public class TrackRecordingServiceTestLocation {
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// Reset service (if some previous test failed)
|
||||
if (service.isRecording() || service.isPaused()) {
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
|
||||
// Ensure that the database is empty after every test
|
||||
contentProviderUtils.deleteAllTracks(context);
|
||||
}
|
||||
@@ -95,12 +92,12 @@ public class TrackRecordingServiceTestLocation {
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// when
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 2, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 3, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 4, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 6, 15);
|
||||
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -108,8 +105,17 @@ public class TrackRecordingServiceTestLocation {
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(6, trackPoints.size());
|
||||
assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01);
|
||||
assertEquals(8, trackPoints.size());
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 4),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@@ -117,14 +123,15 @@ public class TrackRecordingServiceTestLocation {
|
||||
public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception {
|
||||
// given
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertNotNull(trackId);
|
||||
|
||||
// when
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 2, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 5, 15);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 6, 15);
|
||||
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -132,8 +139,13 @@ public class TrackRecordingServiceTestLocation {
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(2, trackPoints.size());
|
||||
assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01);
|
||||
assertEquals(4, trackPoints.size());
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
// @MediumTest
|
||||
@@ -174,10 +186,13 @@ public class TrackRecordingServiceTestLocation {
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(3, trackPoints.size());
|
||||
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
|
||||
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
|
||||
assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01);
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@@ -200,14 +215,17 @@ public class TrackRecordingServiceTestLocation {
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(4, trackPoints.size());
|
||||
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
|
||||
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
|
||||
assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted.
|
||||
assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01);
|
||||
assertEquals(6, trackPoints.size());
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 5), //TODO Check why this trackPoint is inserted.
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testOnLocationChangedAsync_idle_withSensorData() throws Exception {
|
||||
@@ -230,12 +248,12 @@ public class TrackRecordingServiceTestLocation {
|
||||
});
|
||||
|
||||
// when
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 0, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
|
||||
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 0);
|
||||
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -243,13 +261,17 @@ public class TrackRecordingServiceTestLocation {
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(6, trackPoints.size());
|
||||
assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
|
||||
assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01);
|
||||
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
|
||||
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
|
||||
assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01);
|
||||
assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01);
|
||||
assertEquals(8, trackPoints.size());
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 4),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@@ -272,12 +294,26 @@ public class TrackRecordingServiceTestLocation {
|
||||
|
||||
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
|
||||
assertEquals(7, trackPoints.size());
|
||||
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
|
||||
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
|
||||
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
|
||||
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
|
||||
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01);
|
||||
assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01);
|
||||
assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01);
|
||||
assertTrackPoints(List.of(
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
|
||||
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_AUTOMATIC, 2),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
|
||||
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_START_AUTOMATIC, 4),
|
||||
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
|
||||
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
|
||||
), trackPoints);
|
||||
}
|
||||
|
||||
private void assertTrackPoints(List<Pair<TrackPoint.Type, Object>> typeAndAccuracy, List<TrackPoint> actual) {
|
||||
assertEquals(typeAndAccuracy.size(), actual.size());
|
||||
for (int i = 0; i < typeAndAccuracy.size(); i++) {
|
||||
assertEquals(typeAndAccuracy.get(i).first, actual.get(i).getType());
|
||||
if (typeAndAccuracy.get(i).second != null) {
|
||||
assertEquals((Integer) typeAndAccuracy.get(i).second, actual.get(i).getAccuracy(), 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -239,7 +240,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
assertEquals(trackId, track.getId());
|
||||
TrackStatistics trackStatistics = track.getTrackStatistics();
|
||||
assertNotNull(trackStatistics);
|
||||
assertTrue(trackStatistics.getStartTime_ms() > 0);
|
||||
assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
|
||||
assertTrue(trackStatistics.getStartTime().isAfter(Instant.ofEpochMilli(0)));
|
||||
assertTrue(trackStatistics.getStopTime().isAfter(trackStatistics.getStartTime()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class HandlerServerTest {
|
||||
@Test
|
||||
public void sendTrackPoint() throws InterruptedException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
int accuracy = 50;
|
||||
|
||||
// when
|
||||
|
||||
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.services.handlers;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -32,6 +34,12 @@ public class LocationHandlerTest {
|
||||
@InjectMocks
|
||||
private LocationHandler locationHandler;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Let's use default values.
|
||||
@@ -80,7 +88,6 @@ public class LocationHandlerTest {
|
||||
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
|
||||
|
||||
// when
|
||||
// poor latitude
|
||||
locationHandler.onLocationChanged(createLocation(45f, 35f, prefAccuracy + 1, 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
|
||||
@@ -20,7 +20,11 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* Tests for {@link TrackStatistics}.
|
||||
@@ -39,16 +43,40 @@ public class TrackStatisticsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMerge() {
|
||||
public void testMerge_no_data() {
|
||||
// given
|
||||
TrackStatistics statistics2 = new TrackStatistics();
|
||||
statistics.setStartTime_ms(1000L); // Resulting start time
|
||||
statistics.setStopTime_ms(2500L);
|
||||
statistics2.setStartTime_ms(3000L);
|
||||
statistics2.setStopTime_ms(4000L); // Resulting stop time
|
||||
statistics.setTotalTime(1500L);
|
||||
statistics2.setTotalTime(1000L); // Result: 1500+1000
|
||||
statistics.setMovingTime(700L);
|
||||
statistics2.setMovingTime(600L); // Result: 700+600
|
||||
|
||||
// when
|
||||
statistics.merge(statistics2);
|
||||
|
||||
// then
|
||||
assertNull(statistics.getStartTime());
|
||||
assertNull(statistics.getStopTime());
|
||||
assertEquals(Duration.ofSeconds(0), statistics.getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(0), statistics.getTotalTime());
|
||||
|
||||
assertNull(statistics.getTotalElevationGain());
|
||||
assertNull(statistics.getTotalElevationLoss());
|
||||
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxElevation(), 0.0);
|
||||
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinElevation(), 0.0);
|
||||
assertEquals(0.0, statistics.getMaxSpeed(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageSpeed(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageMovingSpeed(), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMerge() {
|
||||
// given
|
||||
TrackStatistics statistics2 = new TrackStatistics();
|
||||
statistics.setStartTime(Instant.ofEpochMilli(1000)); // Resulting start time
|
||||
statistics.setStopTime(Instant.ofEpochMilli(2500));
|
||||
statistics2.setStartTime(Instant.ofEpochMilli(3000));
|
||||
statistics2.setStopTime(Instant.ofEpochMilli(4000)); // Resulting stop time
|
||||
statistics.setTotalTime(Duration.ofMillis(1500));
|
||||
statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000
|
||||
statistics.setMovingTime(Duration.ofMillis(700));
|
||||
statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics2.setTotalDistance(350.0); // Result: 750+350
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
@@ -60,15 +88,17 @@ public class TrackStatisticsTest {
|
||||
statistics2.setMaxElevation(3575.0); // Resulting max elevation
|
||||
statistics2.setMinElevation(2800.0);
|
||||
|
||||
// when
|
||||
statistics.merge(statistics2);
|
||||
|
||||
assertEquals(1000L, statistics.getStartTime_ms());
|
||||
assertEquals(4000L, statistics.getStopTime_ms());
|
||||
assertEquals(2500L, statistics.getTotalTime());
|
||||
assertEquals(1300L, statistics.getMovingTime());
|
||||
// then
|
||||
assertEquals(Instant.ofEpochMilli(1000), statistics.getStartTime());
|
||||
assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime());
|
||||
assertEquals(Duration.ofMillis(2500), statistics.getTotalTime());
|
||||
assertEquals(Duration.ofMillis(1300), statistics.getMovingTime());
|
||||
assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
|
||||
assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
|
||||
assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
|
||||
assertEquals(statistics.getTotalDistance() / statistics.getMovingTime().getSeconds(), statistics.getMaxSpeed(), 0.001);
|
||||
assertEquals(1200.0, statistics.getMinElevation(), 0.001);
|
||||
assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
|
||||
}
|
||||
@@ -76,14 +106,14 @@ public class TrackStatisticsTest {
|
||||
@Test
|
||||
public void testGetAverageSpeed() {
|
||||
statistics.setTotalDistance(1000.0);
|
||||
statistics.setTotalTime(50000); // in milliseconds
|
||||
statistics.setTotalTime(Duration.ofMillis(50000));
|
||||
assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAverageMovingSpeed() {
|
||||
statistics.setTotalDistance(1000.0);
|
||||
statistics.setMovingTime(20000); // in milliseconds
|
||||
statistics.setMovingTime(Duration.ofMillis(20000));
|
||||
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TrackStatisticsUpdaterTest {
|
||||
|
||||
@Test
|
||||
public void addTrackPoint() {
|
||||
// given
|
||||
TestDataUtil.TrackData data = TestDataUtil.createTestingTrack(new Track.Id(1));
|
||||
|
||||
// when
|
||||
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||
data.trackPoints.forEach(it -> updater.addTrackPoint(it, 50));
|
||||
|
||||
// then
|
||||
TrackStatistics statistics = updater.getTrackStatistics();
|
||||
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
|
||||
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
|
||||
|
||||
assertEquals(2.5, statistics.getMinElevation(), 0.01);
|
||||
assertEquals(27.5, statistics.getMaxElevation(), 0.01);
|
||||
assertEquals(27, statistics.getTotalElevationGain(), 0.01);
|
||||
assertEquals(27.0, statistics.getTotalElevationLoss(), 0.01);
|
||||
|
||||
assertEquals(14.226, statistics.getMaxSpeed(), 0.01);
|
||||
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
|
||||
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -25,13 +26,15 @@ public class AnnouncementUtilsTest {
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void getAnnouncement() {
|
||||
public void getAnnouncement_metric() {
|
||||
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
|
||||
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(600000);
|
||||
stats.setMovingTime(300000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setTotalElevationGain(6000);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", null);
|
||||
@@ -41,42 +44,26 @@ public class AnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval() {
|
||||
public void getAnnouncement_withInterval_metric() {
|
||||
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
|
||||
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(600000);
|
||||
stats.setMovingTime(300000);
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setTotalElevationGain(6000);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
|
||||
int speedId = R.plurals.voiceSpeedKilometersPerHour;
|
||||
double kmPerHour = lastInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH;
|
||||
|
||||
String firstPartMsg = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour";
|
||||
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
|
||||
String msg = firstPartMsg + rateMsg;
|
||||
String expected = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour Lap speed of 51.2 kilometers per hour";
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals(msg, announcement);
|
||||
}
|
||||
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
int count = (int) d;
|
||||
return Math.max(count, 3);
|
||||
}
|
||||
assertEquals(expected, announcement);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@@ -40,55 +41,32 @@ public class StringUtilsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatDateTimeIso8601(long)}.
|
||||
*/
|
||||
@Test
|
||||
public void testFormatDateTimeIso8601() {
|
||||
assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatElapsedTime(long)}.
|
||||
* Tests {@link StringUtils#formatElapsedTime(Duration)}.
|
||||
*/
|
||||
@Test
|
||||
public void testformatElapsedTime() {
|
||||
// 1 second
|
||||
assertEquals("00:01", StringUtils.formatElapsedTime(1000));
|
||||
// 10 seconds
|
||||
assertEquals("00:10", StringUtils.formatElapsedTime(10000));
|
||||
// 1 minute
|
||||
assertEquals("01:00", StringUtils.formatElapsedTime(60000));
|
||||
// 10 minutes
|
||||
assertEquals("10:00", StringUtils.formatElapsedTime(600000));
|
||||
// 1 hour
|
||||
assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000));
|
||||
// 10 hours
|
||||
assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000));
|
||||
// 100 hours
|
||||
assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000));
|
||||
assertEquals("00:01", StringUtils.formatElapsedTime(Duration.ofMillis(1000)));
|
||||
assertEquals("00:10", StringUtils.formatElapsedTime(Duration.ofMillis(10000)));
|
||||
assertEquals("01:00", StringUtils.formatElapsedTime(Duration.ofMillis(60000)));
|
||||
assertEquals("10:00", StringUtils.formatElapsedTime(Duration.ofMillis(600000)));
|
||||
assertEquals("1:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(3600000)));
|
||||
assertEquals("10:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(36000000)));
|
||||
assertEquals("100:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(360000000)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatElapsedTimeWithHour(long)}.
|
||||
* Tests {@link StringUtils#formatElapsedTimeWithHour(Duration)}.
|
||||
*/
|
||||
@Test
|
||||
public void testformatElapsedTimeWithHour() {
|
||||
// 1 second
|
||||
assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
|
||||
// 10 seconds
|
||||
assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000));
|
||||
// 1 minute
|
||||
assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000));
|
||||
// 10 minutes
|
||||
assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000));
|
||||
// 1 hour
|
||||
assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000));
|
||||
// 10 hours
|
||||
assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000));
|
||||
// 100 hours
|
||||
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000));
|
||||
assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(1000)));
|
||||
assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(10000)));
|
||||
assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(60000)));
|
||||
assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(600000)));
|
||||
assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(3600000)));
|
||||
assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(36000000)));
|
||||
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(360000000)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,29 +148,7 @@ public class StringUtilsTest {
|
||||
|
||||
// This comparision tends to be flaky (difference of 1ms)
|
||||
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
|
||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime)) <= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#getTimeParts(long)} with a positive number.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeParts_positive() {
|
||||
int[] parts = StringUtils.getTimeParts(61000);
|
||||
assertEquals(1, parts[0]);
|
||||
assertEquals(1, parts[1]);
|
||||
assertEquals(0, parts[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#getTimeParts(long)} with a negative number.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeParts_negative() {
|
||||
int[] parts = StringUtils.getTimeParts(-61000);
|
||||
assertEquals(-1, parts[0]);
|
||||
assertEquals(-1, parts[1]);
|
||||
assertEquals(0, parts[2]);
|
||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toEpochMilli()) <= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -42,7 +42,7 @@ import static org.junit.Assert.assertEquals;
|
||||
public class TrackNameUtilsTest {
|
||||
|
||||
private static final Track.Id TRACK_ID = new Track.Id(1L);
|
||||
private static final long START_TIME = 1288213406000L;
|
||||
private static final Instant START_TIME = Instant.ofEpochMilli(1288213406000L);
|
||||
|
||||
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@@ -62,7 +62,7 @@ public class TrackNameUtilsTest {
|
||||
public void testTrackName_date_iso_8601() {
|
||||
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
|
||||
assertEquals(simpleDateFormat.format(new Date(START_TIME)), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
|
||||
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TrackPointUtilsTest {
|
||||
|
||||
@Test
|
||||
public void fixTime_none() {
|
||||
// given
|
||||
long time = System.currentTimeMillis();
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
trackPoint.setTime(time);
|
||||
|
||||
// when
|
||||
TrackPointUtils.fixTime(trackPoint);
|
||||
|
||||
// then
|
||||
assertEquals(time, trackPoint.getTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixTime_0() {
|
||||
// given
|
||||
long time = System.currentTimeMillis();
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
trackPoint.setTime(0);
|
||||
|
||||
// when
|
||||
TrackPointUtils.fixTime(trackPoint);
|
||||
|
||||
// then
|
||||
assertEquals(time, trackPoint.getTime(), 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixTime_gpsWeekRollover() {
|
||||
// given
|
||||
long time = System.currentTimeMillis();
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
trackPoint.setTime(time - 1024 * UnitConversions.ONE_WEEK_MS);
|
||||
|
||||
// when
|
||||
TrackPointUtils.fixTime(trackPoint);
|
||||
|
||||
// then
|
||||
assertEquals(time, trackPoint.getTime());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -23,19 +25,12 @@ public class AggregatedStatisticsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
* Create a TrackStatistics object.
|
||||
*
|
||||
* @param totalDistance distance in meters.
|
||||
* @param totalTime total time in milliseconds.
|
||||
* @return TrackStatistics object.
|
||||
*/
|
||||
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setStartTime_ms(1000L); // Resulting start time
|
||||
statistics.setStopTime_ms(1000L + totalTime);
|
||||
statistics.setTotalTime(totalTime);
|
||||
statistics.setMovingTime(totalTime);
|
||||
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
|
||||
statistics.setStopTime(Instant.ofEpochMilli(1000L + totalTime));
|
||||
statistics.setTotalTime(Duration.ofMillis(totalTime));
|
||||
statistics.setMovingTime(Duration.ofMillis(totalTime));
|
||||
statistics.setTotalDistance(totalDistance);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(50.0); // Resulting max speed
|
||||
@@ -68,7 +63,7 @@ public class AggregatedStatisticsTest {
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,7 +117,7 @@ public class AggregatedStatisticsTest {
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime * 2L, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,19 +151,19 @@ public class AggregatedStatisticsTest {
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,28 +214,28 @@ public class AggregatedStatisticsTest {
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime * 5, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime * 5), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Running.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime * 2, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Walking.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime * 2, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Driving.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Check order
|
||||
|
||||
@@ -25,12 +25,12 @@ public class IntervalStatisticsTest {
|
||||
}
|
||||
|
||||
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints.get(0).getTime());
|
||||
for (TrackPoint tp : trackPoints) {
|
||||
trackStatisticsUpdater.addTrackPoint(tp, 0);
|
||||
}
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
for (TrackPoint tp : trackPoints) {
|
||||
trackStatisticsUpdater.addTrackPoint(tp, 0);
|
||||
}
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
@@ -116,17 +116,17 @@ public class IntervalStatisticsTest {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
|
||||
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
||||
double totalDistance = 0d;
|
||||
long totalTime = 0L;
|
||||
float totalTime = 0L;
|
||||
float totalGain = 0f;
|
||||
for (IntervalStatistics.Interval i : intervalList) {
|
||||
totalDistance += i.getDistance_m();
|
||||
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
|
||||
totalTime += i.getDistance_m() / i.getSpeed_ms();
|
||||
totalGain += i.getGain_m();
|
||||
}
|
||||
|
||||
// then
|
||||
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
|
||||
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
|
||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This hidden file is here to ensure there is a res folder.
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:gpxtpx="http://www.garmin.com/xmlschemes/TrackPointExtension/v2" xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1" xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.1" creator="OpenTracks (Debug)" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
|
||||
<metadata>
|
||||
<name><![CDATA[2021-01-07 22:51]]></name>
|
||||
<desc><![CDATA[the description]]></desc>
|
||||
</metadata>
|
||||
<wpt lat="3" lon="14">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:51:59.179Z</time>
|
||||
<name><![CDATA[Marker 1]]></name>
|
||||
<desc><![CDATA[Marker 1 desc]]></desc>
|
||||
<type><![CDATA[Marker 1 category]]></type>
|
||||
</wpt>
|
||||
<wpt lat="3" lon="14.002">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:52:01.010Z</time>
|
||||
<name><![CDATA[Marker 2]]></name>
|
||||
<desc><![CDATA[Marker 2 desc]]></desc>
|
||||
<type><![CDATA[Marker 2 category]]></type>
|
||||
</wpt>
|
||||
<trk>
|
||||
<name><![CDATA[2021-01-07 22:51]]></name>
|
||||
<desc><![CDATA[the description]]></desc>
|
||||
<type><![CDATA[the category]]></type>
|
||||
<extensions>
|
||||
<topografix:color>c0c0c0</topografix:color>
|
||||
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
|
||||
</extensions>
|
||||
<trkseg>
|
||||
<trkpt lat="3" lon="14">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:51:59.179Z</time>
|
||||
</trkpt>
|
||||
<trkpt lat="3" lon="14.001">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:52:00.653Z</time>
|
||||
</trkpt>
|
||||
<trkpt lat="3" lon="14.002">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:52:01.010Z</time>
|
||||
</trkpt>
|
||||
<trkpt lat="100" lon="0">
|
||||
<time>2021-01-07T21:52:02.658Z</time>
|
||||
</trkpt>
|
||||
|
||||
<trkpt lat="200" lon="0">
|
||||
<time>2021-01-07T21:52:03.873Z</time>
|
||||
</trkpt>
|
||||
<trkpt lat="3" lon="14.003">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:52:04.103Z</time>
|
||||
</trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="end"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="waypoint"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<description><![CDATA[]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle>
|
||||
<color>7f0000ff</color>
|
||||
<width>4</width>
|
||||
</LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon>
|
||||
<href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href>
|
||||
</Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start">
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon>
|
||||
<href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href>
|
||||
</Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels" />
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="end">
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon>
|
||||
<href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href>
|
||||
</Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels" />
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="waypoint">
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon>
|
||||
<href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href>
|
||||
</Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels" />
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
|
||||
<open>1</open>
|
||||
<Placemark>
|
||||
<name><![CDATA[Split 0]]></name>
|
||||
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
|
||||
Total time: 00:00
|
||||
Moving time: 00:00
|
||||
Average speed: 0.00 km/h (0.0 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 0:00 min/km (0:00 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
|
||||
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Max grade: 0 %
|
||||
Min grade: 0 %
|
||||
Recorded: 6/23 11:29]]></description>
|
||||
<TimeStamp>
|
||||
<when>1970-01-01T00:00:00Z</when>
|
||||
</TimeStamp>
|
||||
<styleUrl>#waypoint</styleUrl>
|
||||
<Point>
|
||||
<coordinates>180.0,100.0</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
|
||||
<description />
|
||||
<TimeStamp>
|
||||
<when>2020-11-28T17:06:22.401Z</when>
|
||||
</TimeStamp>
|
||||
<styleUrl>#start</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<description><![CDATA[Test Track]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type">
|
||||
<value><![CDATA[unknown]]></value>
|
||||
</Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:22.401Z</when>
|
||||
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
|
||||
<when>2020-11-28T17:06:25.448Z</when>
|
||||
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
|
||||
<when>2020-11-28T17:06:47.888Z</when>
|
||||
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.5391469</gx:value>
|
||||
<gx:value>0.15777911</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:55.861Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
|
||||
<when>2020-11-28T17:06:56.905Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
|
||||
<when>2020-11-28T17:07:20.870Z</when>
|
||||
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
|
||||
<description><![CDATA[OpenTracks (Debug)
|
||||
|
||||
Name: 2020-11-28 18:06
|
||||
Activity type: unknown
|
||||
Description: -
|
||||
Total distance: 0.01 km (0.0 mi)
|
||||
Total time: 01:04
|
||||
Moving time: 00:00
|
||||
Average speed: 0.49 km/h (0.3 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 122:30 min/km (197:08 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: 469 m (1540 ft)
|
||||
Min elevation: 419 m (1376 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Recorded: 11/28 18:06]]></description>
|
||||
<TimeStamp>
|
||||
<when>2020-11-28T17:07:20.870Z</when>
|
||||
</TimeStamp>
|
||||
<styleUrl>#end</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="end"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="waypoint"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
|
||||
<open>1</open>
|
||||
<Placemark>
|
||||
<name><![CDATA[Split 0]]></name>
|
||||
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
|
||||
Total time: 00:00
|
||||
Moving time: 00:00
|
||||
Average speed: 0.00 km/h (0.0 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 0:00 min/km (0:00 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
|
||||
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Max grade: 0 %
|
||||
Min grade: 0 %
|
||||
Recorded: 6/23 11:29
|
||||
]]></description>
|
||||
<TimeStamp><when>1970-01-01T00:00:00Z</when></TimeStamp>
|
||||
<styleUrl>#waypoint</styleUrl>
|
||||
<Point>
|
||||
<coordinates>180.0,100.0</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
|
||||
<description><![CDATA[]]></description>
|
||||
<TimeStamp><when>2020-11-28T17:06:22.401Z</when></TimeStamp>
|
||||
<styleUrl>#start</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<description><![CDATA[Test Track]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:22.401Z</when>
|
||||
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
|
||||
<when>2020-11-28T17:06:25.448Z</when>
|
||||
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
|
||||
<when>2020-11-28T17:06:47.888Z</when>
|
||||
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.5391469</gx:value>
|
||||
<gx:value>0.15777911</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:55.861Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
|
||||
<when>2020-11-28T17:06:56.905Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
|
||||
<when>2020-11-28T17:07:20.870Z</when>
|
||||
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
|
||||
<description><![CDATA[OpenTracks (Debug)
|
||||
|
||||
Name: 2020-11-28 18:06
|
||||
Activity type: unknown
|
||||
Description: -
|
||||
Total distance: 0.01 km (0.0 mi)
|
||||
Total time: 01:04
|
||||
Moving time: 00:00
|
||||
Average speed: 0.49 km/h (0.3 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 122:30 min/km (197:08 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: 469 m (1540 ft)
|
||||
Min elevation: 419 m (1376 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Recorded: 11/28 18:06
|
||||
]]></description>
|
||||
<TimeStamp><when>2020-11-28T17:07:20.870Z</when></TimeStamp>
|
||||
<styleUrl>#end</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">OpenTracks (Debug)</string>
|
||||
<string name="app_name" translatable="false">OpenTracks (Debug)</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,8 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
@@ -52,9 +54,8 @@ public class TrackController implements View.OnTouchListener {
|
||||
private final TransitionDrawable transition;
|
||||
|
||||
private boolean isRecording;
|
||||
private boolean isPaused;
|
||||
private long totalTime = 0;
|
||||
private long totalTimeTimestamp = 0;
|
||||
private boolean isRecordingPaused;
|
||||
private Duration totalTime;
|
||||
private boolean isResumed = false;
|
||||
|
||||
private final Callback callback;
|
||||
@@ -62,8 +63,9 @@ public class TrackController implements View.OnTouchListener {
|
||||
// A runnable to update the total time.
|
||||
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed && isRecording && !isPaused) {
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
||||
if (isResumed && isRecording && !isRecordingPaused) {
|
||||
updateTotalTime();
|
||||
setTotalTime();
|
||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,7 @@ public class TrackController implements View.OnTouchListener {
|
||||
|
||||
viewBinding.trackControllerRecord.setOnTouchListener(this);
|
||||
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
|
||||
if (buttonDelay != null || (isRecording && !isPaused)) {
|
||||
if (buttonDelay != null || (isRecording && !isRecordingPaused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ public class TrackController implements View.OnTouchListener {
|
||||
//To pause a recording
|
||||
if (viewBinding.trackControllerRecord.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& isRecording && !isPaused) {
|
||||
&& isRecording && !isRecordingPaused) {
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
@@ -164,10 +166,11 @@ public class TrackController implements View.OnTouchListener {
|
||||
|
||||
public void update(boolean recording, boolean paused) {
|
||||
if (!isResumed) {
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
return;
|
||||
}
|
||||
isRecording = recording;
|
||||
isPaused = paused;
|
||||
isRecordingPaused = paused;
|
||||
boolean visible = alwaysShow || isRecording;
|
||||
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -176,26 +179,26 @@ public class TrackController implements View.OnTouchListener {
|
||||
return;
|
||||
}
|
||||
|
||||
viewBinding.trackControllerRecord.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
|
||||
if (isRecording && !isRecordingPaused) {
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
|
||||
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
|
||||
|
||||
viewBinding.trackControllerStop.setEnabled(isRecording);
|
||||
|
||||
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (isRecording) {
|
||||
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
|
||||
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
|
||||
showStatusSetDefaultText();
|
||||
}
|
||||
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
if (isRecording) {
|
||||
totalTime = getTotalTime();
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
if (!isPaused) {
|
||||
totalTimeTimestamp = System.currentTimeMillis();
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
updateTotalTime();
|
||||
setTotalTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,16 +220,21 @@ public class TrackController implements View.OnTouchListener {
|
||||
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total time for the current recording track.
|
||||
*/
|
||||
private long getTotalTime() {
|
||||
private void updateTotalTime() {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
|
||||
if (trackRecordingService != null) {
|
||||
totalTime = trackRecordingService.getTotalTime();
|
||||
}
|
||||
}
|
||||
|
||||
private void setTotalTime() {
|
||||
if (totalTime != null) {
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
}
|
||||
}
|
||||
|
||||
private void showStatusSetDefaultText() {
|
||||
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
viewBinding.trackControllerStatus.setText(isRecordingPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
@@ -44,6 +44,8 @@ import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||
@@ -224,7 +226,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
String icon = cursor.getString(iconIndex);
|
||||
int iconId = TrackIconUtils.getIconDrawable(icon);
|
||||
String name = cursor.getString(nameIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
||||
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
|
||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||
int markerCount = cursor.getInt(markerCountIndex);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
|
||||
@@ -33,7 +33,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
private ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
//TODO Should be Marker.Id
|
||||
private final ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
|
||||
private boolean scroll = false;
|
||||
|
||||
@@ -111,8 +112,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
|
||||
return;
|
||||
}
|
||||
|
||||
long id = cursor.getLong(cursor.getColumnIndex(MarkerColumns._ID));
|
||||
String photoUrl = cursor.getString(cursor.getColumnIndex(MarkerColumns.PHOTOURL));
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
|
||||
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
|
||||
|
||||
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
|
||||
if (hasPhoto) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -13,19 +12,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
public static final int HEART_RATE_TYPE = 0;
|
||||
public static final int CADENCE_TYPE = 1;
|
||||
public static final int POWER_TYPE = 2;
|
||||
|
||||
private List<Pair<Integer, SensorData>> sensorDataList;
|
||||
private List<SensorDataModel> sensorDataList;
|
||||
private final Context context;
|
||||
|
||||
public SensorsAdapter(Context context) {
|
||||
@@ -42,9 +33,8 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
|
||||
int type = sensorDataList.get(position).first;
|
||||
SensorData sensorData = sensorDataList.get(position).second;
|
||||
viewHolder.setData(sensorData, type);
|
||||
SensorDataModel sensorDataModel = sensorDataList.get(position);
|
||||
viewHolder.setData(sensorDataModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,12 +46,7 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return sensorDataList.get(position).first;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
|
||||
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
|
||||
if (sensorDataList == data) {
|
||||
return null;
|
||||
}
|
||||
@@ -89,70 +74,18 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
unit = itemView.findViewById(R.id.stats_sensor_unit);
|
||||
}
|
||||
|
||||
public void setData(SensorData sensorData, int type) {
|
||||
switch (type) {
|
||||
case HEART_RATE_TYPE:
|
||||
setHeartRateSensorData((SensorDataHeartRate) sensorData);
|
||||
break;
|
||||
case CADENCE_TYPE:
|
||||
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
|
||||
break;
|
||||
case POWER_TYPE:
|
||||
setPowerSensorData((SensorDataCyclingPower) sensorData);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown sensor type");
|
||||
}
|
||||
}
|
||||
public void setData(SensorDataModel sensorDataModel) {
|
||||
String sensorName = sensorDataModel.getSensorName();
|
||||
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
|
||||
|
||||
private void setHeartRateSensorData(SensorDataHeartRate data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
||||
}
|
||||
this.label.setText(context.getString(sensorDataModel.getLabelId()));
|
||||
if (sensorName == null) {
|
||||
this.sensorValue.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.sensorValue.setText(sensorName);
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_beats_per_minute);
|
||||
}
|
||||
|
||||
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasCadence_rpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_cadence));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
|
||||
}
|
||||
|
||||
private void setPowerSensorData(SensorDataCyclingPower data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasPower_w() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_power));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_power);
|
||||
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ChartPoint {
|
||||
}
|
||||
timeOrDistance = distance;
|
||||
} else {
|
||||
timeOrDistance = trackStatistics.getTotalTime();
|
||||
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
||||
}
|
||||
|
||||
elevation = trackStatisticsUpdater.getSmoothedElevation();
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -740,7 +741,7 @@ public class ChartView extends View {
|
||||
* @param spacing the spacing between x axis and marker
|
||||
*/
|
||||
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat, int spacing) {
|
||||
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((long) value);
|
||||
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((Duration.ofMillis((long) value)));
|
||||
Rect rect = getRect(xAxisMarkerPaint, marker);
|
||||
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@@ -146,7 +148,7 @@ public class DescriptionGenerator {
|
||||
}
|
||||
|
||||
// Recorded time
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
|
||||
builder.append(lineBreak);
|
||||
|
||||
return builder.toString();
|
||||
@@ -177,7 +179,7 @@ public class DescriptionGenerator {
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writeTime(Duration time, StringBuilder builder, int resId, String lineBreak) {
|
||||
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
@@ -40,13 +40,12 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Track data hub.
|
||||
* Receives data from {@link de.dennisguse.opentracks.content.provider.CustomContentProvider} and distributes it to {@link TrackDataListener} after some processing.
|
||||
*
|
||||
* <p>
|
||||
* {@link TrackPoint}s are filtered/downsampled with a dynamic sampling frequency.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
@@ -143,7 +142,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
|
||||
PreferencesUtils.register(context, this);
|
||||
onSharedPreferenceChanged(null, null);
|
||||
runInHandlerThread(() -> {
|
||||
handler.post(() -> {
|
||||
if (started) {
|
||||
loadDataForAll();
|
||||
}
|
||||
@@ -174,7 +173,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
}
|
||||
|
||||
public void loadTrack(final @NonNull Track.Id trackId) {
|
||||
runInHandlerThread(() -> {
|
||||
handler.post(() -> {
|
||||
if (trackId.equals(selectedTrackId)) {
|
||||
Log.i(TAG, "Not reloading track " + trackId.getId());
|
||||
return;
|
||||
@@ -190,7 +189,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
|
||||
runInHandlerThread(() -> {
|
||||
handler.post(() -> {
|
||||
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
|
||||
if (started) {
|
||||
loadDataForListener(trackDataListener);
|
||||
@@ -204,7 +203,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
|
||||
runInHandlerThread(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
|
||||
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +222,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
|
||||
runInHandlerThread(() -> {
|
||||
handler.post(() -> {
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
}
|
||||
@@ -314,9 +313,6 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
do {
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
if (!LocationUtils.isValidLocation(marker.getLocation())) {
|
||||
continue;
|
||||
}
|
||||
for (TrackDataListener trackDataListener : trackDataListeners) {
|
||||
trackDataListener.onNewMarker(marker);
|
||||
}
|
||||
@@ -362,15 +358,14 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
|
||||
TrackPoint.Id lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
|
||||
int samplingFrequency = -1;
|
||||
boolean includeNextPoint = false;
|
||||
|
||||
|
||||
TrackPoint.Id next;
|
||||
TrackPoint.Id next = null;
|
||||
if (localLastSeenTrackPointIdId != null) {
|
||||
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
|
||||
}
|
||||
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, null)) {
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
@@ -390,19 +385,15 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { //This can be split markers (not anymore supported feature)
|
||||
includeNextPoint = true;
|
||||
|
||||
// Also include the last point if the selected track is not recording.
|
||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint);
|
||||
}
|
||||
} else {
|
||||
// Also include the last point if the selected track is not recording.
|
||||
if (includeNextPoint || (localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
includeNextPoint = false;
|
||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint);
|
||||
}
|
||||
} else {
|
||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint);
|
||||
}
|
||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,20 +421,4 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
firstSeenTrackPointId = null;
|
||||
lastSeenTrackPointId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run in the handler thread.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
@Deprecated //TODO: Why actually catch this problem: I guess it would be better to fail hard.
|
||||
@VisibleForTesting
|
||||
private void runInHandlerThread(Runnable runnable) {
|
||||
if (handler == null) {
|
||||
// Use a Throwable to ensure the stack trace is logged.
|
||||
Log.d(TAG, "handler is null.", new Throwable());
|
||||
return;
|
||||
}
|
||||
handler.post(runnable);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
@@ -39,43 +41,49 @@ public final class Marker {
|
||||
private String category = "";
|
||||
private String icon = "";
|
||||
private final Track.Id trackId;
|
||||
|
||||
private final Instant time;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Float accuracy;
|
||||
private Double altitude_m;
|
||||
private Float bearing;
|
||||
|
||||
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||
private double length = 0.0;
|
||||
private long duration = 0;
|
||||
private final Location location;
|
||||
private long duration = 0; //TODO Duration
|
||||
|
||||
@Deprecated //TODO Make an URI instead of String
|
||||
private String photoUrl = "";
|
||||
|
||||
public Marker(@Nullable Track.Id trackId) {
|
||||
public Marker(@Nullable Track.Id trackId, Instant time) {
|
||||
this.trackId = trackId;
|
||||
location = null;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Marker(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this(trackId, trackPoint.getLocation());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//TODO Used by AbstractFileImporter to create an intermediate marker before saving a new one into the database.
|
||||
public Marker(@NonNull Location location) {
|
||||
this(null, location);
|
||||
}
|
||||
|
||||
public Marker(@NonNull Track.Id trackId, @NonNull Location location) {
|
||||
this.location = location;
|
||||
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this.trackId = trackId;
|
||||
|
||||
this.time = trackPoint.getTime();
|
||||
|
||||
if (!trackPoint.hasLocation())
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
this.latitude = trackPoint.getLatitude();
|
||||
this.longitude = trackPoint.getLongitude();
|
||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
|
||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||
}
|
||||
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull Location location, String photoUrl) {
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
this.icon = icon;
|
||||
this.trackId = trackId;
|
||||
this.length = length;
|
||||
this.duration = duration;
|
||||
this.location = location;
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
@@ -91,6 +99,10 @@ public final class Marker {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -128,6 +140,83 @@ public final class Marker {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null || longitude != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
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 (hasAccuracy()) {
|
||||
location.setAccuracy(accuracy);
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude_m);
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public boolean hasAccuracy() {
|
||||
return accuracy != null;
|
||||
}
|
||||
|
||||
public Float getAccuracy() {
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
public void setAccuracy(Float accuracy) {
|
||||
this.accuracy = accuracy;
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude_m != null;
|
||||
}
|
||||
|
||||
public Double getAltitude() {
|
||||
return altitude_m;
|
||||
}
|
||||
|
||||
public void setAltitude(double altitude_m) {
|
||||
this.altitude_m = altitude_m;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
|
||||
public Float getBearing() {
|
||||
return bearing;
|
||||
}
|
||||
|
||||
public void setBearing(float bearing) {
|
||||
this.bearing = bearing;
|
||||
}
|
||||
|
||||
public double getLength() {
|
||||
return length;
|
||||
}
|
||||
@@ -144,11 +233,6 @@ public final class Marker {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public String getPhotoUrl() {
|
||||
return photoUrl;
|
||||
}
|
||||
|
||||
@@ -16,25 +16,72 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* This class extends the standard Android location with extra information.
|
||||
* <p>
|
||||
* NOTE: default location will be latitude=0.0 and longitude=0.0.
|
||||
* For {@link TrackPoint}s with `type == null` this is not meaningful.
|
||||
* <p>
|
||||
* NOTE: For Locations provided by the GPS.
|
||||
* We are replacing the GPS-provided time using the system time.
|
||||
* Then we have the same timestamps for the user-driven events (aka start, pause, resume) and restore {@link de.dennisguse.opentracks.stats.TrackStatistics}.
|
||||
* Drawbacks:
|
||||
* * GPS-provided timestamp might be more precise (but also have GPS week rollover)
|
||||
* * System clock might be changed (and thus non-monotonic)
|
||||
* TODO: if these might be a problem, we need to store both timestamps.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
//TODO Merge constructors by use case; we have too many.
|
||||
public class TrackPoint {
|
||||
|
||||
private TrackPoint.Id id;
|
||||
|
||||
private final Location location;
|
||||
private Instant time;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Float accuracy;
|
||||
private Double altitude_m;
|
||||
private Float speed_mps;
|
||||
private Float bearing;
|
||||
|
||||
public enum Type {
|
||||
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
||||
|
||||
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
|
||||
TRACKPOINT(0), //Just GPS data.
|
||||
|
||||
SEGMENT_END_MANUAL(1); //End of a segment
|
||||
|
||||
public final int type_db;
|
||||
|
||||
Type(int type_db) {
|
||||
this.type_db = type_db;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name() + "(" + type_db + ")";
|
||||
}
|
||||
|
||||
public static Type getById(int id) {
|
||||
for (Type e : values()) {
|
||||
if (e.type_db == id) return e;
|
||||
}
|
||||
|
||||
throw new RuntimeException("unknown id: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Type type;
|
||||
|
||||
private Float heartRate_bpm = null;
|
||||
private Float cyclingCadence_rpm = null;
|
||||
@@ -42,61 +89,76 @@ public class TrackPoint {
|
||||
private Float elevationGain = null;
|
||||
private Float elevationLoss = null;
|
||||
|
||||
public TrackPoint() {
|
||||
this.location = new Location("");
|
||||
public TrackPoint(@NonNull Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Location location) {
|
||||
this.location = location;
|
||||
this(Type.TRACKPOINT);
|
||||
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
this.altitude_m = location.getAltitude();
|
||||
this.speed_mps = location.getSpeed();
|
||||
this.accuracy = location.getAccuracy();
|
||||
|
||||
setTime(Instant.now());
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
this.location = trackPoint.getLocation();
|
||||
|
||||
this.heartRate_bpm = trackPoint.getHeartRate_bpm();
|
||||
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
|
||||
this.power = trackPoint.getPower();
|
||||
|
||||
this.elevationGain = trackPoint.getElevationGain();
|
||||
this.elevationLoss = trackPoint.getElevationLoss();
|
||||
public TrackPoint(@NonNull Type type, Instant time) {
|
||||
this(type);
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
|
||||
location = new Location(LocationManager.GPS_PROVIDER);
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
if (altitude != null) {
|
||||
location.setAltitude(altitude);
|
||||
}
|
||||
location.setTime(time);
|
||||
public TrackPoint(double latitude, double longitude, Double altitude, Instant time) {
|
||||
this(Type.TRACKPOINT);
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.altitude_m = altitude;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Deprecated //See #316
|
||||
public static TrackPoint createPause() {
|
||||
return createPauseWithTime(System.currentTimeMillis());
|
||||
public static TrackPoint createSegmentStartManual() {
|
||||
return createSegmentStartManualWithTime(Instant.now());
|
||||
}
|
||||
|
||||
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
|
||||
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
|
||||
}
|
||||
|
||||
@Deprecated //See #316
|
||||
public static TrackPoint createPauseWithTime(long time) {
|
||||
Location pause = new Location(LocationManager.GPS_PROVIDER);
|
||||
pause.setLongitude(0);
|
||||
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
|
||||
pause.setTime(time);
|
||||
return new TrackPoint(pause);
|
||||
public static TrackPoint createSegmentStartAutomatic() {
|
||||
return createSegmentStartAutomaticWithTime(Instant.now());
|
||||
}
|
||||
|
||||
@Deprecated //See #316
|
||||
public static TrackPoint createResume() {
|
||||
return createResumeWithTime(System.currentTimeMillis());
|
||||
public static TrackPoint createSegmentStartAutomaticWithTime(Instant time) {
|
||||
return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time);
|
||||
}
|
||||
|
||||
@Deprecated //See #316
|
||||
public static TrackPoint createResumeWithTime(long time) {
|
||||
Location resume = new Location(LocationManager.GPS_PROVIDER);
|
||||
resume.setLongitude(0);
|
||||
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
|
||||
resume.setTime(time);
|
||||
return new TrackPoint(resume);
|
||||
public static TrackPoint createSegmentEnd() {
|
||||
return createSegmentEndWithTime(Instant.now());
|
||||
}
|
||||
|
||||
public static TrackPoint createSegmentEndWithTime(Instant time) {
|
||||
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(@NonNull Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public boolean isSegmentStart() {
|
||||
return type == Type.SEGMENT_START_AUTOMATIC || type == Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
|
||||
public boolean isSegmentEnd() {
|
||||
return type == Type.SEGMENT_END_MANUAL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,8 +173,44 @@ public class TrackPoint {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public boolean hasLocation() {
|
||||
return latitude != null && longitude != null;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
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 (hasAccuracy()) {
|
||||
location.setAccuracy(accuracy);
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude_m);
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
@@ -120,7 +218,7 @@ public class TrackPoint {
|
||||
return elevationGain != null;
|
||||
}
|
||||
|
||||
public float getElevationGain() {
|
||||
public Float getElevationGain() {
|
||||
return elevationGain;
|
||||
}
|
||||
|
||||
@@ -140,88 +238,72 @@ public class TrackPoint {
|
||||
this.elevationLoss = elevationLoss;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return location.getLatitude();
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
location.setLatitude(latitude);
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return location.getLongitude();
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
location.setLongitude(longitude);
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return location.getTime();
|
||||
}
|
||||
|
||||
public void setTime(long time) {
|
||||
location.setTime(time);
|
||||
public void setTime(Instant time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return location.hasAltitude();
|
||||
return altitude_m != null;
|
||||
}
|
||||
|
||||
public double getAltitude() {
|
||||
return location.getAltitude();
|
||||
return altitude_m;
|
||||
}
|
||||
|
||||
public void setAltitude(double altitude) {
|
||||
location.setAltitude(altitude);
|
||||
this.altitude_m = altitude;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return location.hasSpeed();
|
||||
return speed_mps != null;
|
||||
}
|
||||
|
||||
public float getSpeed() {
|
||||
return location.getSpeed();
|
||||
return speed_mps;
|
||||
}
|
||||
|
||||
public void setSpeed(float speed) {
|
||||
location.setSpeed(speed);
|
||||
public void setSpeed(Float speed) {
|
||||
this.speed_mps = speed;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return location.hasBearing();
|
||||
return bearing != null;
|
||||
}
|
||||
|
||||
public float getBearing() {
|
||||
return location.getBearing();
|
||||
return bearing;
|
||||
}
|
||||
|
||||
public void setBearing(float bearing) {
|
||||
location.setBearing(bearing);
|
||||
public void setBearing(Float bearing) {
|
||||
this.bearing = bearing;
|
||||
}
|
||||
|
||||
public boolean hasAccuracy() {
|
||||
return location.hasAccuracy();
|
||||
return accuracy != null;
|
||||
}
|
||||
|
||||
public float getAccuracy() {
|
||||
return location.getAccuracy();
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
public void setAccuracy(float horizontalAccuracy) {
|
||||
location.setAccuracy(horizontalAccuracy);
|
||||
this.accuracy = horizontalAccuracy;
|
||||
}
|
||||
|
||||
public float distanceTo(@NonNull TrackPoint dest) {
|
||||
return location.distanceTo(dest.getLocation());
|
||||
return getLocation().distanceTo(dest.getLocation());
|
||||
}
|
||||
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
return location.bearingTo(dest.getLocation());
|
||||
return getLocation().bearingTo(dest.getLocation());
|
||||
}
|
||||
|
||||
public float bearingTo(@NonNull Location dest) {
|
||||
return location.bearingTo(dest);
|
||||
return getLocation().bearingTo(dest);
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
@@ -268,7 +350,16 @@ public class TrackPoint {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "time=" + getTime() + ": lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy();
|
||||
String result = "time=" + getTime() + " (type=" + getType() + ")";
|
||||
if (!hasLocation()) {
|
||||
return result;
|
||||
}
|
||||
result += ": lat=" + getLatitude() + " lng=" + getLongitude();
|
||||
if (!hasAccuracy()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result + " acc=" + getAccuracy();
|
||||
}
|
||||
|
||||
public static class Id {
|
||||
|
||||
@@ -37,13 +37,11 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
|
||||
// Columns
|
||||
String TRACKID = "trackid";
|
||||
// See {@link TrackPoint.Type}
|
||||
String TYPE = "type";
|
||||
|
||||
String LONGITUDE = "longitude";
|
||||
String LATITUDE = "latitude";
|
||||
@Deprecated
|
||||
double PAUSE_LATITUDE = 100.0;
|
||||
@Deprecated
|
||||
double RESUME_LATITUDE = 200.0;
|
||||
|
||||
String TIME = "time";
|
||||
String ALTITUDE = "elevation";
|
||||
@@ -71,6 +69,7 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
+ SENSOR_POWER + " FLOAT, "
|
||||
+ ELEVATION_GAIN + " FLOAT, "
|
||||
+ ELEVATION_LOSS + " FLOAT, "
|
||||
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
|
||||
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
*/
|
||||
class CachedTrackPointsIndexes {
|
||||
final int idIndex;
|
||||
final int typeIndex;
|
||||
final int longitudeIndex;
|
||||
final int latitudeIndex;
|
||||
final int timeIndex;
|
||||
@@ -24,6 +25,7 @@ class CachedTrackPointsIndexes {
|
||||
|
||||
CachedTrackPointsIndexes(Cursor cursor) {
|
||||
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
|
||||
typeIndex = cursor.getColumnIndex(TrackPointsColumns.TYPE);
|
||||
longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
|
||||
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
|
||||
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
|
||||
|
||||
@@ -29,6 +29,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
@@ -42,6 +44,7 @@ import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
@@ -63,8 +66,6 @@ public class ContentProviderUtils {
|
||||
// The base URI for the app's content provider.
|
||||
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
|
||||
|
||||
// Maximum number of markers that will be loaded at one time.
|
||||
public static final int MAX_LOADED_MARKERS = 10000;
|
||||
private static final String ID_SEPARATOR = ",";
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
@@ -79,20 +80,6 @@ public class ContentProviderUtils {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a track: removes markers and trackPoints.
|
||||
* Only keeps the track id.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
@Deprecated //TODO Figure out why we need this.
|
||||
public void clearTrack(Track.Id trackId) {
|
||||
deleteTrackPointsAndMarkers(trackId);
|
||||
Track track = new Track();
|
||||
track.setId(trackId);
|
||||
updateTrack(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Track} from a cursor.
|
||||
*
|
||||
@@ -134,19 +121,19 @@ public class ContentProviderUtils {
|
||||
track.setCategory(cursor.getString(categoryIndex));
|
||||
}
|
||||
if (!cursor.isNull(startTimeIndex)) {
|
||||
trackStatistics.setStartTime_ms(cursor.getLong(startTimeIndex));
|
||||
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
|
||||
}
|
||||
if (!cursor.isNull(stopTimeIndex)) {
|
||||
trackStatistics.setStopTime_ms(cursor.getLong(stopTimeIndex));
|
||||
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
|
||||
}
|
||||
if (!cursor.isNull(totalDistanceIndex)) {
|
||||
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
|
||||
}
|
||||
if (!cursor.isNull(totalTimeIndex)) {
|
||||
trackStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
|
||||
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
|
||||
}
|
||||
if (!cursor.isNull(movingTimeIndex)) {
|
||||
trackStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
|
||||
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
|
||||
}
|
||||
if (!cursor.isNull(maxSpeedIndex)) {
|
||||
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
|
||||
@@ -199,13 +186,7 @@ public class ContentProviderUtils {
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
|
||||
}
|
||||
|
||||
private void deleteTrackPointsAndMarkers(@NonNull Track.Id trackId) {
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
|
||||
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, TrackPointsColumns.TRACKID + "=?", selectionArgs);
|
||||
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns.TRACKID + "=?", selectionArgs);
|
||||
}
|
||||
|
||||
//TODO Only use for tests; also move to tests.
|
||||
public List<Track> getTracks() {
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
|
||||
@@ -284,7 +265,6 @@ public class ContentProviderUtils {
|
||||
ContentValues values = new ContentValues();
|
||||
TrackStatistics trackStatistics = track.getTrackStatistics();
|
||||
|
||||
// Value < 0 indicates no id is available
|
||||
if (track.getId() != null) {
|
||||
values.put(TracksColumns._ID, track.getId().getId());
|
||||
}
|
||||
@@ -292,11 +272,15 @@ public class ContentProviderUtils {
|
||||
values.put(TracksColumns.NAME, track.getName());
|
||||
values.put(TracksColumns.DESCRIPTION, track.getDescription());
|
||||
values.put(TracksColumns.CATEGORY, track.getCategory());
|
||||
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime_ms());
|
||||
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime_ms());
|
||||
if (trackStatistics.getStartTime() != null) {
|
||||
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
|
||||
}
|
||||
if (trackStatistics.getStopTime() != null) {
|
||||
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
|
||||
}
|
||||
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
|
||||
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime());
|
||||
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime());
|
||||
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
|
||||
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
|
||||
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
|
||||
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
|
||||
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
|
||||
@@ -326,27 +310,23 @@ public class ContentProviderUtils {
|
||||
int bearingIndex = cursor.getColumnIndexOrThrow(MarkerColumns.BEARING);
|
||||
int photoUrlIndex = cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL);
|
||||
|
||||
Location location = new Location("");
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||
Marker marker = new Marker(trackId, Instant.ofEpochMilli(cursor.getLong(timeIndex)));
|
||||
|
||||
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
|
||||
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
|
||||
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
|
||||
}
|
||||
if (!cursor.isNull(timeIndex)) {
|
||||
location.setTime(cursor.getLong(timeIndex));
|
||||
marker.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
|
||||
marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
|
||||
}
|
||||
if (!cursor.isNull(altitudeIndex)) {
|
||||
location.setAltitude(cursor.getFloat(altitudeIndex));
|
||||
marker.setAltitude(cursor.getFloat(altitudeIndex));
|
||||
}
|
||||
if (!cursor.isNull(accuracyIndex)) {
|
||||
location.setAccuracy(cursor.getFloat(accuracyIndex));
|
||||
marker.setAccuracy(cursor.getFloat(accuracyIndex));
|
||||
}
|
||||
if (!cursor.isNull(bearingIndex)) {
|
||||
location.setBearing(cursor.getFloat(bearingIndex));
|
||||
marker.setBearing(cursor.getFloat(bearingIndex));
|
||||
}
|
||||
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||
Marker marker = new Marker(trackId, location);
|
||||
|
||||
if (!cursor.isNull(idIndex)) {
|
||||
marker.setId(new Marker.Id(cursor.getLong(idIndex)));
|
||||
}
|
||||
@@ -439,7 +419,8 @@ public class ContentProviderUtils {
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks
|
||||
//TODO Move to testing package
|
||||
@Deprecated
|
||||
public int getMarkerCount(Track.Id trackId) {
|
||||
String[] projection = new String[]{"count(*) AS count"};
|
||||
String selection = MarkerColumns.TRACKID + "=?";
|
||||
@@ -502,18 +483,17 @@ public class ContentProviderUtils {
|
||||
values.put(MarkerColumns.LENGTH, marker.getLength());
|
||||
values.put(MarkerColumns.DURATION, marker.getDuration());
|
||||
|
||||
Location location = marker.getLocation();
|
||||
values.put(MarkerColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
|
||||
values.put(MarkerColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
|
||||
values.put(MarkerColumns.TIME, location.getTime());
|
||||
if (location.hasAltitude()) {
|
||||
values.put(MarkerColumns.ALTITUDE, location.getAltitude());
|
||||
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
|
||||
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
|
||||
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
|
||||
if (marker.hasAltitude()) {
|
||||
values.put(MarkerColumns.ALTITUDE, marker.getAltitude());
|
||||
}
|
||||
if (location.hasAccuracy()) {
|
||||
values.put(MarkerColumns.ACCURACY, location.getAccuracy());
|
||||
if (marker.hasAccuracy()) {
|
||||
values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
|
||||
}
|
||||
if (location.hasBearing()) {
|
||||
values.put(MarkerColumns.BEARING, location.getBearing());
|
||||
if (marker.hasBearing()) {
|
||||
values.put(MarkerColumns.BEARING, marker.getBearing());
|
||||
}
|
||||
|
||||
values.put(MarkerColumns.PHOTOURL, marker.getPhotoUrl());
|
||||
@@ -544,10 +524,8 @@ public class ContentProviderUtils {
|
||||
* @param indexes the cached trackPoints indexes
|
||||
*/
|
||||
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
if (!cursor.isNull(indexes.longitudeIndex)) {
|
||||
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
|
||||
}
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex)));
|
||||
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
|
||||
|
||||
if (!cursor.isNull(indexes.longitudeIndex)) {
|
||||
trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
|
||||
@@ -556,7 +534,7 @@ public class ContentProviderUtils {
|
||||
trackPoint.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
|
||||
}
|
||||
if (!cursor.isNull(indexes.timeIndex)) {
|
||||
trackPoint.setTime(cursor.getLong(indexes.timeIndex));
|
||||
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
|
||||
}
|
||||
if (!cursor.isNull(indexes.altitudeIndex)) {
|
||||
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex));
|
||||
@@ -696,7 +674,7 @@ public class ContentProviderUtils {
|
||||
*/
|
||||
@Deprecated
|
||||
public TrackPoint getLastValidTrackPoint(Track.Id trackId) {
|
||||
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
|
||||
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TYPE + " IN (" + TrackPoint.Type.SEGMENT_START_AUTOMATIC.type_db + "," + TrackPoint.Type.TRACKPOINT.type_db + "))";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
|
||||
return findTrackPointBy(selection, selectionArgs);
|
||||
}
|
||||
@@ -715,16 +693,19 @@ public class ContentProviderUtils {
|
||||
/**
|
||||
* Creates the {@link ContentValues} for a {@link TrackPoint}.
|
||||
*
|
||||
* @param trackPoint the trackPointstats_pace_km#87
|
||||
* @param trackPoint the trackPoint
|
||||
* @param trackId the track id
|
||||
*/
|
||||
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TrackPointsColumns.TRACKID, trackId.getId());
|
||||
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
|
||||
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
|
||||
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
|
||||
|
||||
values.put(TrackPointsColumns.TIME, trackPoint.getTime());
|
||||
if (trackPoint.hasLocation()) {
|
||||
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
|
||||
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
|
||||
}
|
||||
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
|
||||
if (trackPoint.hasAltitude()) {
|
||||
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
|
||||
}
|
||||
@@ -855,4 +836,32 @@ public class ContentProviderUtils {
|
||||
public static String[] parseTrackIdsFromUri(Uri url) {
|
||||
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
|
||||
}
|
||||
|
||||
public SensorStatistics getSensorStats(Track.Id trackId) {
|
||||
if (trackId == null) {
|
||||
return null;
|
||||
}
|
||||
String[] projection = {
|
||||
"MAX(" + TrackPointsColumns.SENSOR_HEARTRATE + ") max_hr",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_HEARTRATE + ") avg_hr",
|
||||
"MAX(" + TrackPointsColumns.SENSOR_CADENCE + ") max_cadence",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_CADENCE + ") avg_cadence",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_POWER + ") avg_power"
|
||||
};
|
||||
String selection = TrackPointsColumns.TRACKID + "=?";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
|
||||
SensorStatistics sensorStatistics = null;
|
||||
try (Cursor cursor = getTrackPointCursor(projection, selection, selectionArgs, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
sensorStatistics = new SensorStatistics(
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_hr")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_hr")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_cadence")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_cadence")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_power")) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
return sensorStatistics;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,10 +329,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
|
||||
if (!hasLatitude || !hasLongitude || !hasTime) {
|
||||
if (!hasTime) {
|
||||
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
|
||||
}
|
||||
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
|
||||
@@ -343,10 +341,6 @@ public class CustomContentProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
private Uri insertTrack(Uri url, ContentValues contentValues) {
|
||||
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
|
||||
if (!hasStartTime) {
|
||||
throw new IllegalArgumentException("Both start time and start id values are required.");
|
||||
}
|
||||
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
|
||||
if (rowId >= 0) {
|
||||
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
|
||||
|
||||
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 29;
|
||||
private static final int DATABASE_VERSION = 30;
|
||||
|
||||
@VisibleForTesting
|
||||
public static final String DATABASE_NAME = "database.db";
|
||||
@@ -385,8 +385,8 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
// TrackPoints; identical to upgradeFrom27to28()
|
||||
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain FROM trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain FROM trackpoints_old");
|
||||
db.execSQL("DROP TABLE trackpoints_old");
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
@@ -221,7 +221,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext());
|
||||
final TrackExporter trackExporter = getTrackFileFormat(uri).createTrackExporter(getContext());
|
||||
|
||||
PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
/**
|
||||
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
|
||||
*/
|
||||
//TODO Remove batching; that should be handled by the database/contentprovider (i.e., already in place as we use a cursor)!
|
||||
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
|
||||
|
||||
private static final String TAG = TrackPointIterator.class.getSimpleName();
|
||||
@@ -22,7 +23,6 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
|
||||
private TrackPoint.Id lastTrackPointId = null;
|
||||
private Cursor cursor;
|
||||
|
||||
|
||||
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.trackId = trackId;
|
||||
|
||||
@@ -4,25 +4,29 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
|
||||
public class SensorData {
|
||||
public class SensorData<T> {
|
||||
|
||||
protected T value;
|
||||
|
||||
private final String sensorAddress;
|
||||
private final String sensorName;
|
||||
|
||||
private final long timestamp_ms;
|
||||
private final Instant timestamp_ms;
|
||||
|
||||
SensorData(String sensorAddress) {
|
||||
this(sensorAddress, null, System.currentTimeMillis());
|
||||
this(sensorAddress, null);
|
||||
}
|
||||
|
||||
SensorData(String sensorAddress, String sensorName) {
|
||||
this(sensorAddress, sensorName, System.currentTimeMillis());
|
||||
this(sensorAddress, sensorName, Instant.now());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SensorData(String sensorAddress, String sensorName, long timestamp_ms) {
|
||||
SensorData(String sensorAddress, String sensorName, Instant timestamp_ms) {
|
||||
this.sensorAddress = sensorAddress;
|
||||
this.sensorName = sensorName;
|
||||
this.timestamp_ms = timestamp_ms;
|
||||
@@ -42,10 +46,18 @@ public class SensorData {
|
||||
return sensorName != null ? sensorName : sensorAddress;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*/
|
||||
public boolean isRecent() {
|
||||
return timestamp_ms + BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS > System.currentTimeMillis();
|
||||
return Instant.now().isBefore(timestamp_ms.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -20,11 +21,10 @@ public final class SensorDataCycling {
|
||||
private SensorDataCycling() {
|
||||
}
|
||||
|
||||
public static class Cadence extends SensorData {
|
||||
public static class Cadence extends SensorData<Float> {
|
||||
|
||||
private final Long crankRevolutionsCount; // UINT32
|
||||
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float cadence_rpm;
|
||||
|
||||
public Cadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
@@ -57,24 +57,16 @@ public final class SensorDataCycling {
|
||||
return crankRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasCadence_rpm() {
|
||||
return cadence_rpm != null;
|
||||
}
|
||||
|
||||
public float getCadence_rpm() {
|
||||
return cadence_rpm;
|
||||
}
|
||||
|
||||
public void compute(Cadence previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
cadence_rpm = null;
|
||||
value = null;
|
||||
} else {
|
||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
float cadence_ms = crankDiff / timeDiff_ms;
|
||||
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
|
||||
value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +74,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
return "cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,11 +90,10 @@ public final class SensorDataCycling {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Speed extends SensorData {
|
||||
public static class Speed extends SensorData<Float> {
|
||||
|
||||
private final Integer wheelRevolutionsCount; // UINT16
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float speed_mps;
|
||||
|
||||
public Speed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
@@ -128,24 +119,16 @@ public final class SensorDataCycling {
|
||||
return wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasSpeed_mps() {
|
||||
return speed_mps != null;
|
||||
}
|
||||
|
||||
public float getSpeed_mps() {
|
||||
return speed_mps;
|
||||
}
|
||||
|
||||
public void compute(Speed previous, int wheel_circumference_mm) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
speed_mps = null;
|
||||
value = null;
|
||||
} else {
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
|
||||
speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
|
||||
value = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +136,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
return "speed=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,23 +152,19 @@ public final class SensorDataCycling {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CadenceAndSpeed extends SensorData {
|
||||
|
||||
private final Cadence cadence;
|
||||
private final Speed speed;
|
||||
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, Speed>> {
|
||||
|
||||
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.cadence = cadence;
|
||||
this.speed = speed;
|
||||
this.value = new Pair<>(cadence, speed);
|
||||
}
|
||||
|
||||
public Cadence getCadence() {
|
||||
return cadence;
|
||||
return this.value != null ? this.value.first : null;
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
return this.value != null ? this.value.second : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataCyclingPower extends SensorData {
|
||||
|
||||
private final Float power_w;
|
||||
public class SensorDataCyclingPower extends SensorData<Float> {
|
||||
|
||||
public SensorDataCyclingPower(String address) {
|
||||
super(address);
|
||||
this.power_w = null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower(String name, String address, float power_w) {
|
||||
super(name, address);
|
||||
this.power_w = power_w;
|
||||
}
|
||||
|
||||
public boolean hasPower_w() {
|
||||
return power_w != null;
|
||||
}
|
||||
|
||||
public float getPower_w() {
|
||||
return power_w;
|
||||
this.value = power_w;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "power=" + power_w;
|
||||
return "power=" + value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataHeartRate extends SensorData {
|
||||
|
||||
private final Float heartRate_bpm;
|
||||
public class SensorDataHeartRate extends SensorData<Float> {
|
||||
|
||||
public SensorDataHeartRate(String address) {
|
||||
super(address);
|
||||
heartRate_bpm = null;
|
||||
}
|
||||
|
||||
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
||||
super(name, address);
|
||||
this.heartRate_bpm = heartRate_bpm;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate_bpm() {
|
||||
return heartRate_bpm != null;
|
||||
}
|
||||
|
||||
public Float getHeartRate_bpm() {
|
||||
return heartRate_bpm;
|
||||
this.value = heartRate_bpm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "heart=" + heartRate_bpm;
|
||||
return "heart=" + value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,19 +50,19 @@ public final class SensorDataSet {
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
if (heartRate != null) {
|
||||
trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm());
|
||||
trackPoint.setHeartRate_bpm(heartRate.getValue());
|
||||
}
|
||||
|
||||
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
|
||||
trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm());
|
||||
if (cyclingCadence != null && cyclingCadence.hasValue()) {
|
||||
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue());
|
||||
}
|
||||
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
|
||||
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasValue()) {
|
||||
trackPoint.setSpeed(cyclingSpeed.getValue());
|
||||
}
|
||||
|
||||
if (cyclingPower != null && cyclingPower.hasPower_w()) {
|
||||
trackPoint.setPower(cyclingPower.getPower_w());
|
||||
if (cyclingPower != null && cyclingPower.hasValue()) {
|
||||
trackPoint.setPower(cyclingPower.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
@@ -57,7 +56,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private static final String KEY_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
|
||||
|
||||
public static Fragment newInstance(boolean chartByDistance) {
|
||||
public static ChartFragment newInstance(boolean chartByDistance) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
|
||||
|
||||
@@ -71,8 +70,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
// Stats gathered from the received data
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater;
|
||||
private long startTime;
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
|
||||
private ChartBinding viewBinding;
|
||||
|
||||
@@ -198,7 +196,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
if (track == null || track.getTrackStatistics() == null) {
|
||||
startTime = -1L;
|
||||
category = "";
|
||||
return;
|
||||
}
|
||||
@@ -209,14 +206,13 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
chartView.applyReportSpeed();
|
||||
}
|
||||
startTime = track.getTrackStatistics().getStartTime_ms();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
trackStatisticsUpdater = startTime != -1L ? new TrackStatisticsUpdater(startTime) : null;
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
pendingPoints.clear();
|
||||
chartView.reset();
|
||||
runOnUiThread(() -> {
|
||||
@@ -261,7 +257,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
@Override
|
||||
public void onNewMarker(Marker marker) {
|
||||
if (isResumed() && marker != null && LocationUtils.isValidLocation(marker.getLocation())) {
|
||||
if (isResumed() && marker != null) {
|
||||
chartView.addMarker(marker);
|
||||
}
|
||||
}
|
||||
@@ -327,11 +323,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
return new ChartPoint(trackStatisticsUpdater, trackPoint, chartByDistance, chartView.getMetricUnits());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setTrackStatisticsUpdater(long time) {
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater(time);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setChartView(ChartView view) {
|
||||
chartView = view;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
@@ -253,16 +252,11 @@ public class MarkerDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void setLocation(boolean addShadow) {
|
||||
Location location = marker.getLocation();
|
||||
String value;
|
||||
if (location == null) {
|
||||
value = null;
|
||||
} else {
|
||||
value = "[" + getString(R.string.stats_latitude) + " "
|
||||
+ StringUtils.formatCoordinate(location.getLatitude()) + ", "
|
||||
+ getString(R.string.stats_longitude) + " "
|
||||
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
|
||||
}
|
||||
String value = "[" + getString(R.string.stats_latitude) + " "
|
||||
+ StringUtils.formatCoordinate(marker.getLatitude()) + ", "
|
||||
+ getString(R.string.stats_longitude) + " "
|
||||
+ StringUtils.formatCoordinate(marker.getLongitude()) + "]";
|
||||
|
||||
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,23 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a recorded {@link Track}.
|
||||
@@ -49,6 +57,8 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
private static final String TRACK_ID_KEY = "trackId";
|
||||
|
||||
private TrackStatistics trackStatistics;
|
||||
private SensorStatistics sensorStatistics;
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
private String category = "";
|
||||
private Track.Id trackId;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -67,12 +77,6 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
||||
@@ -82,6 +86,22 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||
sensorsRecyclerView.setAdapter(sensorsAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -112,8 +132,10 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
if (isResumed()) {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
trackStatistics = track != null ? track.getTrackStatistics() : null;
|
||||
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
category = track != null ? track.getCategory() : "";
|
||||
updateUI();
|
||||
updateSensorUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -196,4 +218,26 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
viewBinding.statsElevationLossUnit.setText(parts.second);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSensorUI() {
|
||||
if (sensorStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorStatistics.hasHeartRate()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
|
||||
}
|
||||
if (sensorStatistics.hasCadence()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
|
||||
}
|
||||
if (sensorStatistics.hasPower()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
|
||||
}
|
||||
if (sensorDataList.size() > 0) {
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,7 +26,6 @@ import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a currently recording {@link Track}.
|
||||
@@ -191,8 +192,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
|
||||
if (trackPoint != null) {
|
||||
boolean hasFix = !LocationUtils.isLocationOld(trackPoint.getLocation());
|
||||
if (trackPoint != null && trackPoint.hasLocation()) {
|
||||
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
|
||||
|
||||
if (!hasFix) {
|
||||
lastTrackPoint = null;
|
||||
@@ -271,15 +272,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
} else {
|
||||
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
|
||||
if (sensorDataSet != null) {
|
||||
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorDataSet.getHeartRate() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
||||
}
|
||||
if (sensorDataSet.getCyclingCadence() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
||||
}
|
||||
if(sensorDataSet.getCyclingPower() != null) {
|
||||
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
||||
}
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
setSpeedSensorData(sensorDataSet);
|
||||
@@ -332,8 +333,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
|
||||
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
|
||||
|
||||
if (data.hasSpeed_mps() && data.isRecent()) {
|
||||
setSpeed(data.getSpeed_mps());
|
||||
if (data.hasValue() && data.isRecent()) {
|
||||
setSpeed(data.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,26 +408,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
private void updateTotalTime() {
|
||||
long totalTime;
|
||||
Duration totalTime = lastTrackStatistics.getTotalTime();
|
||||
if (isSelectedTrackRecording()) {
|
||||
totalTime = calculateTotalTime();
|
||||
} else {
|
||||
totalTime = lastTrackStatistics.getTotalTime();
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
totalTime = trackRecordingService.getTotalTime();
|
||||
}
|
||||
}
|
||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return time from service.
|
||||
* If service isn't bound then use lastTrackStatistics for calculate it.
|
||||
*/
|
||||
private long calculateTotalTime() {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
return trackRecordingService.getTotalTime();
|
||||
} else {
|
||||
return System.currentTimeMillis() - lastTrackStatistics.getStopTime_ms() + lastTrackStatistics.getTotalTime();
|
||||
}
|
||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||
}
|
||||
|
||||
private void setLocationValues() {
|
||||
@@ -453,12 +443,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
|
||||
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
|
||||
if (showCoordinate) {
|
||||
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
|
||||
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
|
||||
String latitudeText = getContext().getString(R.string.value_unknown);
|
||||
String longitudeText = getContext().getString(R.string.value_unknown);
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
|
||||
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
|
||||
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
|
||||
}
|
||||
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||
|
||||
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
|
||||
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
|
||||
viewBinding.statsLongitudeValue.setText(longitudeText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackWriter;
|
||||
|
||||
/**
|
||||
* Definition of all possible track formats.
|
||||
@@ -20,8 +18,8 @@ public enum TrackFileFormat {
|
||||
|
||||
KML_ONLY_TRACK {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, false, false, false);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new KMLTrackExporter(context, false, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,8 +33,8 @@ public enum TrackFileFormat {
|
||||
},
|
||||
KML_WITH_TRACKDETAIL {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, true, false, false);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new KMLTrackExporter(context, true, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,8 +48,8 @@ public enum TrackFileFormat {
|
||||
},
|
||||
KML_WITH_TRACKDETAIL_AND_SENSORDATA {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, true, true, false);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new KMLTrackExporter(context, true, true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,16 +62,12 @@ public enum TrackFileFormat {
|
||||
}
|
||||
},
|
||||
KMZ_ONLY_TRACK {
|
||||
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, false, false, exportPhotos);
|
||||
}
|
||||
|
||||
public TrackExporter newTrackExporter(Context context) {
|
||||
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
KMLTrackExporter exporter = new KMLTrackExporter(context, false, false, exportPhotos);
|
||||
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,12 +89,9 @@ public enum TrackFileFormat {
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, true, false, exportPhotos);
|
||||
}
|
||||
|
||||
public TrackExporter newTrackExporter(Context context) {
|
||||
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
KMLTrackExporter exporter = new KMLTrackExporter(context, true, false, exportPhotos);
|
||||
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,8 +114,9 @@ public enum TrackFileFormat {
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, true, true, exportPhotos);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
KMLTrackExporter exporter = new KMLTrackExporter(context, true, true, exportPhotos);
|
||||
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,10 +124,6 @@ public enum TrackFileFormat {
|
||||
return MIME_KMZ;
|
||||
}
|
||||
|
||||
public TrackExporter newTrackExporter(Context context) {
|
||||
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
@@ -152,8 +140,9 @@ public enum TrackFileFormat {
|
||||
private static final boolean exportPhotos = true;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new KmlTrackWriter(context, true, true, exportPhotos);
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
KMLTrackExporter exporter = new KMLTrackExporter(context, true, true, exportPhotos);
|
||||
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,10 +150,6 @@ public enum TrackFileFormat {
|
||||
return MIME_KMZ;
|
||||
}
|
||||
|
||||
public TrackExporter newTrackExporter(Context context) {
|
||||
return newKmzTrackExporter(context, newTrackWriter(context), exportPhotos);
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
@@ -177,8 +162,8 @@ public enum TrackFileFormat {
|
||||
},
|
||||
GPX {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context) {
|
||||
return new GpxTrackWriter(context.getString(R.string.app_name));
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new GPXTrackExporter(new ContentProviderUtils(context), context.getString(R.string.app_name));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -195,31 +180,17 @@ public enum TrackFileFormat {
|
||||
|
||||
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
|
||||
|
||||
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, boolean exportPhotos) {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter);
|
||||
|
||||
return new KmzTrackExporter(context, contentProviderUtils, fileTrackExporter, exportPhotos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type for each format.
|
||||
*/
|
||||
public abstract String getMimeType();
|
||||
|
||||
public TrackExporter newTrackExporter(Context context) {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
TrackWriter trackWriter = newTrackWriter(context);
|
||||
return new FileTrackExporter(contentProviderUtils, trackWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new track writer for the format.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public abstract TrackWriter newTrackWriter(Context context);
|
||||
public abstract TrackExporter createTrackExporter(Context context);
|
||||
|
||||
/**
|
||||
* Returns the file extension for each format.
|
||||
@@ -234,7 +205,7 @@ public enum TrackFileFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of for each format.
|
||||
* Returns the name for each format.
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name().toLowerCase(Locale.US);
|
||||
|
||||
@@ -279,7 +279,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
}
|
||||
|
||||
private void onExportCompleted(Track.Id trackId) {
|
||||
trackIds.remove(trackId);
|
||||
trackIds.remove(trackId);
|
||||
setProgress();
|
||||
if (trackIds.isEmpty()) {
|
||||
onExportEnded();
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
|
||||
/**
|
||||
* Track Writer for writing tracks to an {@link OutputStream}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class FileTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = FileTrackExporter.class.getSimpleName();
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final TrackWriter trackWriter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param contentProviderUtils the content provider utils
|
||||
* @param trackWriter the track writer
|
||||
*/
|
||||
public FileTrackExporter(ContentProviderUtils contentProviderUtils, TrackWriter trackWriter) {
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.trackWriter = trackWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
try {
|
||||
trackWriter.prepare(outputStream);
|
||||
trackWriter.writeHeader(tracks);
|
||||
for (Track track : tracks) {
|
||||
writeMarkers(track);
|
||||
}
|
||||
boolean hasMultipleTracks = tracks.length > 1;
|
||||
if (hasMultipleTracks) {
|
||||
trackWriter.writeMultiTrackBegin();
|
||||
}
|
||||
long startTime = tracks[0].getTrackStatistics().getStartTime_ms();
|
||||
for (Track track : tracks) {
|
||||
long offset = track.getTrackStatistics().getStartTime_ms() - startTime;
|
||||
writeLocations(track, offset);
|
||||
}
|
||||
if (hasMultipleTracks) {
|
||||
trackWriter.writeMultiTrackEnd();
|
||||
}
|
||||
trackWriter.writeFooter();
|
||||
trackWriter.close();
|
||||
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Thread interrupted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMarkers(Track track) throws InterruptedException {
|
||||
/*
|
||||
* TODO: Stream through the markers in chunks.
|
||||
* I am leaving the number of markers very high which should not be a problem, because we don't try to load them into objects all at the same time.
|
||||
*/
|
||||
boolean hasMarkers = false;
|
||||
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, ContentProviderUtils.MAX_LOADED_MARKERS)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
if (!hasMarkers) {
|
||||
trackWriter.writeBeginMarkers(track);
|
||||
hasMarkers = true;
|
||||
}
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
trackWriter.writeMarker(marker);
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasMarkers) {
|
||||
trackWriter.writeEndMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the locations.
|
||||
*/
|
||||
private void writeLocations(Track track, long offset) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
boolean isLastLocationValid = false;
|
||||
TrackPoint lastTrackPoint = null;
|
||||
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
|
||||
setLocationTime(trackPoint, offset);
|
||||
|
||||
boolean isLocationValid = LocationUtils.isValidLocation(trackPoint.getLocation());
|
||||
boolean isSegmentValid = isLocationValid && isLastLocationValid;
|
||||
if (!wroteTrack && isSegmentValid) {
|
||||
// Found the first two consecutive locations that are valid
|
||||
trackWriter.writeBeginTrack(track, lastTrackPoint);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
if (isSegmentValid) {
|
||||
if (!wroteSegment) {
|
||||
// Start a segment
|
||||
trackWriter.writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
|
||||
// Write the previous trackPoint, which we had previously skipped
|
||||
trackWriter.writeTrackPoint(lastTrackPoint);
|
||||
}
|
||||
|
||||
// Write the current trackPoint
|
||||
trackWriter.writeTrackPoint(trackPoint);
|
||||
} else {
|
||||
if (wroteSegment) {
|
||||
trackWriter.writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
}
|
||||
}
|
||||
lastTrackPoint = trackPoint;
|
||||
isLastLocationValid = isLocationValid;
|
||||
}
|
||||
|
||||
if (wroteSegment) {
|
||||
//Close the last segment
|
||||
trackWriter.writeCloseSegment();
|
||||
}
|
||||
|
||||
if (wroteTrack) {
|
||||
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
setLocationTime(lastValidTrackPoint, offset);
|
||||
trackWriter.writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
// Write an empty track
|
||||
trackWriter.writeBeginTrack(track, null);
|
||||
trackWriter.writeEndTrack(track, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a trackPoint time.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
* @param offset the time offset
|
||||
*/
|
||||
private void setLocationTime(TrackPoint trackPoint, long offset) {
|
||||
if (trackPoint != null) {
|
||||
trackPoint.setTime(trackPoint.getTime() - offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.location.Location;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
@@ -26,15 +29,22 @@ import java.util.Locale;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Write track as GPX to a file.
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to GPX.
|
||||
* NOTE:
|
||||
* * does not export {@link TrackPoint} without a latitude/longitude (not supported by GPX 1.1).
|
||||
* * cannot export multiple {@link Track}s - all {@link TrackPoint}s are exported as if they would belong to the first track.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO Export markers
|
||||
public class GpxTrackWriter implements TrackWriter {
|
||||
public class GPXTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = GPXTrackExporter.class.getSimpleName();
|
||||
|
||||
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
@@ -64,19 +74,109 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
CADENCE_FORMAT.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private final String creator;
|
||||
private PrintWriter printWriter;
|
||||
|
||||
public GpxTrackWriter(String creator) {
|
||||
public GPXTrackExporter(ContentProviderUtils contentProviderUtils, String creator) {
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
Track mainTrack = tracks[0];
|
||||
try {
|
||||
prepare(outputStream);
|
||||
writeHeader(mainTrack);
|
||||
|
||||
writeMarkers(mainTrack);
|
||||
|
||||
for (Track track : tracks) {
|
||||
writeLocations(track);
|
||||
}
|
||||
|
||||
writeFooter();
|
||||
close();
|
||||
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Thread interrupted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLocations(Track track) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
|
||||
if (!wroteTrack) {
|
||||
writeBeginTrack(track, trackPoint);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
case SEGMENT_START_MANUAL:
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
|
||||
break;
|
||||
case SEGMENT_END_MANUAL:
|
||||
writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
|
||||
break;
|
||||
case SEGMENT_START_AUTOMATIC:
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
writeTrackPoint(trackPoint);
|
||||
break;
|
||||
case TRACKPOINT:
|
||||
if (!wroteSegment) {
|
||||
// Might happen for older data (pre v3.15.0)
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
}
|
||||
writeTrackPoint(trackPoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (wroteSegment) {
|
||||
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
|
||||
// Anyhow, make sure that the last segment is closed.
|
||||
writeCloseSegment();
|
||||
}
|
||||
|
||||
if (wroteTrack) {
|
||||
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
// Write an empty track
|
||||
writeBeginTrack(track, null);
|
||||
writeEndTrack(track, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
@@ -84,8 +184,8 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track[] tracks) {
|
||||
|
||||
public void writeHeader(Track track) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
printWriter.println("<gpx");
|
||||
@@ -105,39 +205,41 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
|
||||
printWriter.println("<metadata>");
|
||||
|
||||
Track track = tracks[0];
|
||||
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
|
||||
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
|
||||
printWriter.println("</metadata>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gpx>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginMarkers(Track track) {
|
||||
// Do nothing
|
||||
private void writeMarkers(Track track) throws InterruptedException {
|
||||
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
writeMarker(marker);
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndMarkers() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMarker(Marker marker) {
|
||||
if (printWriter != null) {
|
||||
Location location = marker.getLocation();
|
||||
printWriter.println("<wpt " + formatLocation(location) + ">");
|
||||
if (location.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
|
||||
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
|
||||
if (marker.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(marker.getAltitude()) + "</ele>");
|
||||
}
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
|
||||
printWriter.println("<desc>" + StringUtils.formatCData(marker.getDescription()) + "</desc>");
|
||||
printWriter.println("<type>" + StringUtils.formatCData(marker.getCategory()) + "</type>");
|
||||
@@ -145,17 +247,6 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMultiTrackBegin() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMultiTrackEnd() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<trk>");
|
||||
@@ -170,27 +261,25 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</trk>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
printWriter.println("<trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
printWriter.println("</trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTrackPoint(TrackPoint trackPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLocation()) + ">");
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
|
||||
|
||||
if (trackPoint.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
|
||||
}
|
||||
@@ -209,7 +298,7 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
}
|
||||
|
||||
if (trackPoint.hasCyclingCadence()) {
|
||||
printWriter.println("<gpxtpx:cad>" + HEARTRATE_FORMAT.format(trackPoint.getCyclingCadence_rpm()) + "</gpxtpx:cad>");
|
||||
printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCyclingCadence_rpm()) + "</gpxtpx:cad>");
|
||||
}
|
||||
|
||||
if (trackPoint.hasElevationGain()) {
|
||||
@@ -227,12 +316,7 @@ public class GpxTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a location with latitude and longitude coordinates.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private String formatLocation(Location location) {
|
||||
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\"" + COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
|
||||
private String formatLocation(double latitude, double longitude) {
|
||||
return "lat=\"" + COORDINATE_FORMAT.format(latitude) + "\" lon=\"" + COORDINATE_FORMAT.format(longitude) + "\"";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
@@ -13,15 +13,24 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.DescriptionGenerator;
|
||||
@@ -29,15 +38,20 @@ import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Write track as KML to a file.
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class KmlTrackWriter implements TrackWriter {
|
||||
public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = KMLTrackExporter.class.getSimpleName();
|
||||
|
||||
public static final String MARKER_STYLE = "waypoint";
|
||||
private static final String START_STYLE = "start";
|
||||
@@ -74,13 +88,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
|
||||
private TrackPoint startTrackPoint;
|
||||
|
||||
/**
|
||||
* @param context the context
|
||||
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, markers, timing)?
|
||||
* @param exportSensorData should {@link TrackPoint}'s sensor data be exported?
|
||||
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
|
||||
*/
|
||||
public KmlTrackWriter(Context context, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
|
||||
public KMLTrackExporter(Context context, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
|
||||
this.context = context;
|
||||
this.exportTrackDetail = exportTrackDetail;
|
||||
this.exportSensorData = exportSensorData;
|
||||
@@ -89,21 +97,137 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
this.contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(OutputStream outputStream) {
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
try {
|
||||
prepare(outputStream);
|
||||
writeHeader(tracks);
|
||||
for (Track track : tracks) {
|
||||
writeMarkers(track);
|
||||
}
|
||||
boolean hasMultipleTracks = tracks.length > 1;
|
||||
if (hasMultipleTracks) {
|
||||
writeMultiTrackBegin();
|
||||
}
|
||||
//TODO Why use startTime of first track for the others?
|
||||
Instant startTime = tracks[0].getTrackStatistics().getStartTime();
|
||||
for (Track track : tracks) {
|
||||
Duration offset = Duration.between(track.getTrackStatistics().getStartTime(), startTime);
|
||||
writeLocations(track, offset);
|
||||
}
|
||||
if (hasMultipleTracks) {
|
||||
writeMultiTrackEnd();
|
||||
}
|
||||
writeFooter();
|
||||
close();
|
||||
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Thread interrupted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMarkers(Track track) throws InterruptedException {
|
||||
boolean hasMarkers = false;
|
||||
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
if (!hasMarkers) {
|
||||
writeBeginMarkers(track);
|
||||
hasMarkers = true;
|
||||
}
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
writeMarker(marker);
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasMarkers) {
|
||||
writeEndMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLocations(Track track, Duration offset) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
setLocationTime(trackPoint, offset);
|
||||
|
||||
if (!wroteTrack) {
|
||||
writeBeginTrack(track, trackPoint);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
case SEGMENT_START_MANUAL:
|
||||
case SEGMENT_START_AUTOMATIC:
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
writeTrackPoint(trackPoint);
|
||||
wroteSegment = true;
|
||||
break;
|
||||
case SEGMENT_END_MANUAL:
|
||||
if (!wroteSegment) writeOpenSegment();
|
||||
writeTrackPoint(trackPoint);
|
||||
writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
break;
|
||||
case TRACKPOINT:
|
||||
if (!wroteSegment) {
|
||||
// Might happen for older data (pre v3.15.0)
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
}
|
||||
writeTrackPoint(trackPoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (wroteSegment) {
|
||||
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
|
||||
// Anyhow, make sure that the last segment is closed.
|
||||
writeCloseSegment();
|
||||
}
|
||||
|
||||
if (wroteTrack) {
|
||||
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
setLocationTime(lastValidTrackPoint, offset);
|
||||
writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
// Write an empty track
|
||||
writeBeginTrack(track, null);
|
||||
writeEndTrack(track, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
@VisibleForTesting
|
||||
void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track[] tracks) {
|
||||
private void writeHeader(Track[] tracks) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
|
||||
@@ -139,16 +263,14 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
private void writeFooter() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Document>");
|
||||
printWriter.println("</kml>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginMarkers(Track track) {
|
||||
private void writeBeginMarkers(Track track) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<Folder>");
|
||||
if (exportTrackDetail) {
|
||||
@@ -158,15 +280,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndMarkers() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Folder>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMarker(Marker marker) {
|
||||
private void writeMarker(Marker marker) {
|
||||
if (printWriter != null && exportTrackDetail) {
|
||||
boolean existsPhoto = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), marker.getPhotoURI()) != null;
|
||||
if (marker.hasPhoto() && exportPhotos && existsPhoto) {
|
||||
@@ -178,7 +292,13 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeMultiTrackBegin() {
|
||||
private void writeEndMarkers() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Folder>");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMultiTrackBegin() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<Folder id=tour>");
|
||||
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
|
||||
@@ -186,14 +306,13 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeMultiTrackEnd() {
|
||||
private void writeMultiTrackEnd() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Folder>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
|
||||
private void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
|
||||
this.startTrackPoint = startTrackPoint;
|
||||
if (printWriter != null) {
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
@@ -216,8 +335,8 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
|
||||
|
||||
private void writeEndTrack(Track track, TrackPoint endTrackPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gx:MultiTrack>");
|
||||
printWriter.println("</Placemark>");
|
||||
@@ -231,39 +350,41 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
@VisibleForTesting
|
||||
void writeOpenSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<gx:Track>");
|
||||
speedList.clear();
|
||||
powerList.clear();
|
||||
cadenceList.clear();
|
||||
heartRateList.clear();
|
||||
elevationGainList.clear();
|
||||
elevationLossList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
@VisibleForTesting
|
||||
void writeCloseSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<ExtendedData>");
|
||||
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
|
||||
if (speedList.size() > 0) {
|
||||
if (speedList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
|
||||
}
|
||||
if (exportSensorData) {
|
||||
if (powerList.size() > 0) {
|
||||
if (powerList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
|
||||
}
|
||||
if (cadenceList.size() > 0) {
|
||||
if (cadenceList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
|
||||
}
|
||||
if (heartRateList.size() > 0) {
|
||||
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
|
||||
}
|
||||
if (elevationGainList.size() > 0) {
|
||||
if (elevationGainList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN);
|
||||
}
|
||||
if (elevationLossList.size() > 0) {
|
||||
if (elevationLossList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(elevationLossList, EXTENDED_DATA_TYPE_ELEVATION_LOSS);
|
||||
}
|
||||
}
|
||||
@@ -273,35 +394,27 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTrackPoint(TrackPoint trackPoint) {
|
||||
@VisibleForTesting
|
||||
void writeTrackPoint(TrackPoint trackPoint) {
|
||||
if (printWriter != null) {
|
||||
if (exportTrackDetail) {
|
||||
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
|
||||
}
|
||||
|
||||
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
|
||||
|
||||
if (trackPoint.hasSpeed()) {
|
||||
speedList.add(trackPoint.getSpeed());
|
||||
if (trackPoint.hasLocation()) {
|
||||
printWriter.println("<gx:coord>" + (trackPoint.hasLocation() ? getCoordinates(trackPoint.getLocation(), " ") : "") + "</gx:coord>");
|
||||
} else {
|
||||
printWriter.println("<gx:coord/>");
|
||||
}
|
||||
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
|
||||
|
||||
if (exportSensorData) {
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRateList.add(trackPoint.getHeartRate_bpm());
|
||||
}
|
||||
if (trackPoint.hasCyclingCadence()) {
|
||||
cadenceList.add(trackPoint.getCyclingCadence_rpm());
|
||||
}
|
||||
if (trackPoint.hasPower()) {
|
||||
powerList.add(trackPoint.getPower());
|
||||
}
|
||||
if (trackPoint.hasElevationGain()) {
|
||||
elevationGainList.add(trackPoint.getElevationGain());
|
||||
}
|
||||
if (trackPoint.hasElevationLoss()) {
|
||||
elevationLossList.add(trackPoint.getElevationLoss());
|
||||
}
|
||||
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
|
||||
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
|
||||
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
|
||||
|
||||
elevationGainList.add(trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : null);
|
||||
elevationLossList.add(trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +428,12 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
private void writeSimpleArrayData(List<Float> list, String name) {
|
||||
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
|
||||
Float value = list.get(i);
|
||||
if (value == null) {
|
||||
printWriter.println("<gx:value />");
|
||||
} else {
|
||||
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
|
||||
}
|
||||
}
|
||||
printWriter.println("</gx:SimpleArrayData>");
|
||||
}
|
||||
@@ -350,14 +468,14 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
|
||||
printWriter.println("<description>" + StringUtils.formatCData(marker.getDescription()) + "</description>");
|
||||
printWriter.print("<Camera>");
|
||||
printWriter.print("<longitude>" + marker.getLocation().getLongitude() + "</longitude>");
|
||||
printWriter.print("<latitude>" + marker.getLocation().getLatitude() + "</latitude>");
|
||||
printWriter.print("<longitude>" + marker.getLongitude() + "</longitude>");
|
||||
printWriter.print("<latitude>" + marker.getLatitude() + "</latitude>");
|
||||
printWriter.print("<altitude>20</altitude>");
|
||||
printWriter.print("<heading>" + heading + "</heading>");
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(marker.getLocation()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + KmlTrackWriter.MARKER_STYLE + "</styleUrl>");
|
||||
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
|
||||
writeCategory(marker.getCategory());
|
||||
|
||||
if (exportPhotos) {
|
||||
@@ -385,9 +503,9 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
*/
|
||||
private String getTime(Location location) {
|
||||
if (exportTrackDetail) {
|
||||
return StringUtils.formatDateTimeIso8601(location.getTime());
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()));
|
||||
} else {
|
||||
return StringUtils.formatDateTimeIso8601(location.getTime() - startTrackPoint.getTime());
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime() - startTrackPoint.getTime().toEpochMilli()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,4 +591,17 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
|
||||
printWriter.println("</gx:SimpleArrayField>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a trackPoint time.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
* @param offset the time offset
|
||||
*/
|
||||
//TODO Why?
|
||||
private void setLocationTime(TrackPoint trackPoint, Duration offset) {
|
||||
if (trackPoint != null) {
|
||||
trackPoint.setTime(trackPoint.getTime().minus(offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,19 +50,12 @@ public class KmzTrackExporter implements TrackExporter {
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final FileTrackExporter fileTrackExporter;
|
||||
private final KMLTrackExporter fileTrackExporter;
|
||||
|
||||
private final boolean exportPhotos;
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param contentProviderUtils the content provider utils
|
||||
* @param trackExporter the file track exporter
|
||||
*/
|
||||
public KmzTrackExporter(Context context, ContentProviderUtils contentProviderUtils, FileTrackExporter trackExporter, boolean exportPhotos) {
|
||||
public KmzTrackExporter(Context context, ContentProviderUtils contentProviderUtils, KMLTrackExporter trackExporter, boolean exportPhotos) {
|
||||
this.context = context;
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.fileTrackExporter = trackExporter;
|
||||
|
||||
@@ -1,83 +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.io.file.exporter;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
/**
|
||||
* Interface for writing tracks to a file. The expected sequence of calls is:
|
||||
*
|
||||
* <pre>
|
||||
* {@link #prepare(OutputStream)}
|
||||
* {@link #writeHeader(Track[])}
|
||||
* For each track:
|
||||
* {@link #writeBeginMarkers(Track)}
|
||||
* For each marker:
|
||||
* {@link #writeMarker(Marker)}
|
||||
* {@link #writeEndMarkers()}
|
||||
* {@link #writeMultiTrackBegin()}
|
||||
* For each track:
|
||||
* {@link #writeBeginTrack(Track, TrackPoint)}
|
||||
* For each segment:
|
||||
* {@link #writeOpenSegment()}
|
||||
* For each trackPoint in the segment:
|
||||
* {@link #writeTrackPoint(TrackPoint)}
|
||||
* {@link #writeCloseSegment()}
|
||||
* {@link #writeEndTrack(Track, TrackPoint)}
|
||||
* {@link #writeMultiTrackEnd()}
|
||||
* {@link #writeFooter()}
|
||||
* {@link #close()}
|
||||
* </pre>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackWriter {
|
||||
|
||||
void prepare(OutputStream outputStream);
|
||||
|
||||
/**
|
||||
* Closes the output stream.
|
||||
*/
|
||||
void close();
|
||||
|
||||
void writeHeader(Track[] tracks);
|
||||
|
||||
void writeFooter();
|
||||
|
||||
void writeBeginMarkers(Track track);
|
||||
|
||||
void writeEndMarkers();
|
||||
|
||||
void writeMarker(Marker marker);
|
||||
|
||||
void writeMultiTrackBegin();
|
||||
|
||||
void writeMultiTrackEnd();
|
||||
|
||||
void writeBeginTrack(Track track, TrackPoint startTrackPoint);
|
||||
|
||||
void writeEndTrack(Track track, TrackPoint endTrackPoint);
|
||||
|
||||
void writeOpenSegment();
|
||||
|
||||
void writeCloseSegment();
|
||||
|
||||
void writeTrackPoint(TrackPoint trackPoint);
|
||||
}
|
||||
@@ -28,13 +28,14 @@ import org.xml.sax.helpers.DefaultHandler;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -49,7 +50,6 @@ import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Abstract class for file track importers.
|
||||
@@ -120,11 +120,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
@Override
|
||||
public Track.Id importFile(InputStream inputStream) {
|
||||
try {
|
||||
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
saxParser.parse(inputStream, this);
|
||||
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
||||
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
|
||||
return trackIds.get(0);
|
||||
} catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
@@ -156,10 +152,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
int markerPosition = -1;
|
||||
Marker marker = null;
|
||||
TrackPoint trackPoint = null;
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
|
||||
// TODO Should not be necessary anymore?
|
||||
TrackStatisticsUpdater markerTrackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
|
||||
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
|
||||
while (true) {
|
||||
@@ -183,31 +178,30 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
}
|
||||
trackPoint = trackPointIterator.next();
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
markerTrackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
}
|
||||
|
||||
if (marker.getLocation().getTime() > trackPoint.getTime()) {
|
||||
if ((marker.getTime()).isAfter(trackPoint.getTime())) {
|
||||
trackPoint = null;
|
||||
} else if (marker.getLocation().getTime() < trackPoint.getTime()) {
|
||||
} else if (marker.getTime().isBefore(trackPoint.getTime())) {
|
||||
Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
|
||||
marker = null;
|
||||
} else {
|
||||
// The marker trackPoint time matches the track point time
|
||||
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
if (!trackPoint.hasLocation()) {
|
||||
// Invalid trackPoint, load the next trackPoint
|
||||
trackPoint = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid trackPoint
|
||||
if (trackPoint.getLatitude() == marker.getLocation().getLatitude() && trackPoint.getLongitude() == marker.getLocation().getLongitude()) {
|
||||
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
|
||||
String markerDescription = marker.getDescription();
|
||||
String icon = context.getString(R.string.marker_icon_url);
|
||||
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
|
||||
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
|
||||
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime().toMillis();
|
||||
|
||||
// Insert marker
|
||||
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint.getLocation(), marker.getPhotoUrl());
|
||||
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint, marker.getPhotoUrl());
|
||||
contentProviderUtils.insertMarker(newMarker);
|
||||
}
|
||||
|
||||
@@ -252,11 +246,16 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
if (icon != null) {
|
||||
trackData.track.setIcon(icon);
|
||||
}
|
||||
if (trackData.trackStatisticsUpdater == null) {
|
||||
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackData.importTime);
|
||||
trackData.trackStatisticsUpdater.updateTime(trackData.importTime);
|
||||
|
||||
TrackStatisticsUpdater statistics = new TrackStatisticsUpdater();
|
||||
//TODO I guess, we should not filter by recordingDistanceInterval on import; the data is already recorded, so we should not change it.
|
||||
for (TrackPoint trackPoint : trackData.bufferedTrackPoints) {
|
||||
statistics.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
}
|
||||
trackData.track.setTrackStatistics(trackData.trackStatisticsUpdater.getTrackStatistics());
|
||||
if (!statistics.isTrackInitialized()) {
|
||||
throw new ImportParserException("Track did not contain any locations.");
|
||||
}
|
||||
trackData.track.setTrackStatistics(statistics.getTrackStatistics());
|
||||
|
||||
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
|
||||
if (track != null) {
|
||||
@@ -278,35 +277,47 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
trackIds.add(trackId);
|
||||
trackData.track.setId(trackId);
|
||||
|
||||
flushLocations(trackData);
|
||||
flushTrackPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* On track segment start.
|
||||
*/
|
||||
protected void onTrackSegmentStart() {
|
||||
trackData.numberOfSegments++;
|
||||
|
||||
//If not the first segment, add a pause separator if there is at least one location in the last segment.
|
||||
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
|
||||
insertLocation(TrackPoint.createPauseWithTime(trackData.lastLocationInCurrentSegment.getTime()));
|
||||
//If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
|
||||
if (trackData.numberOfSegments > 1
|
||||
&& trackData.lastLocationInCurrentSegment != null
|
||||
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL))
|
||||
) {
|
||||
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
|
||||
}
|
||||
trackData.lastLocationInCurrentSegment = null;
|
||||
}
|
||||
|
||||
protected void onTrackSegmentEnd() {
|
||||
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
|
||||
if (trackPoint == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackPoint.hasLocation()) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addMarker() throws SAXException {
|
||||
// Markers must have a time, else cannot match to the track points
|
||||
if (time == null) {
|
||||
Log.w(TAG, "Marker without time ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
if (!trackPoint.hasLocation()) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
||||
return;
|
||||
}
|
||||
Marker marker = new Marker(trackPoint.getLocation());
|
||||
Marker marker = new Marker(null, trackPoint);
|
||||
|
||||
if (name != null) {
|
||||
marker.setName(name);
|
||||
@@ -324,22 +335,32 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
markers.add(marker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a track point.
|
||||
*/
|
||||
protected TrackPoint getTrackPoint() throws SAXException {
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
if (trackPoint == null) {
|
||||
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
Instant time = trackPoint.getTime();
|
||||
if (trackPoint.getLatitude() == 100) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
trackPoint.setTime(time);
|
||||
} else if (trackPoint.getLatitude() == 200) {
|
||||
//TODO Remove by 31st December 2021.
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
|
||||
trackPoint.setTime(time);
|
||||
|
||||
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate derived attributes from the previous point
|
||||
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
|
||||
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() == null) {
|
||||
if (!trackPoint.hasSpeed()) {
|
||||
long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime();
|
||||
Duration timeDifference = Duration.between(trackData.lastLocationInCurrentSegment.getTime(), trackPoint.getTime());
|
||||
|
||||
// Check for negative time change
|
||||
if (timeDifference <= 0) {
|
||||
if (timeDifference.isNegative()) {
|
||||
Log.w(TAG, "Time difference not positive.");
|
||||
} else {
|
||||
|
||||
@@ -347,35 +368,27 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
* We don't have a speed and bearing in GPX, make something up from the last two points.
|
||||
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
|
||||
*/
|
||||
double duration = timeDifference * UnitConversions.MS_TO_S;
|
||||
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
|
||||
trackPoint.setSpeed((float) speed);
|
||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
||||
float speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / timeDifference.toMillis();
|
||||
trackPoint.setSpeed(speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
||||
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
|
||||
|
||||
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
|
||||
double distanceToLastTrackLocation = trackPoint.distanceTo(trackData.lastLocationInCurrentSegment);
|
||||
if (distanceToLastTrackLocation > maxRecordingDistance) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
}
|
||||
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
}
|
||||
|
||||
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
|
||||
// If not the first segment, add a resume separator before adding the first location.
|
||||
insertLocation(TrackPoint.createResumeWithTime(trackPoint.getTime()));
|
||||
}
|
||||
trackData.lastLocationInCurrentSegment = trackPoint;
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track point.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
protected void insertTrackPoint(TrackPoint trackPoint) {
|
||||
insertLocation(trackPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error message.
|
||||
*
|
||||
@@ -415,11 +428,17 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
* Creates a location.
|
||||
*/
|
||||
private TrackPoint createTrackPoint() throws SAXException {
|
||||
if (latitude == null || longitude == null) {
|
||||
return null;
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
try {
|
||||
trackPoint.setTime(StringUtils.parseTime(time));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint();
|
||||
if (latitude == null || longitude == null) {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
try {
|
||||
trackPoint.setLatitude(Double.parseDouble(latitude));
|
||||
@@ -428,15 +447,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
|
||||
if (time == null) {
|
||||
trackPoint.setTime(trackData.importTime);
|
||||
} else {
|
||||
try {
|
||||
trackPoint.setTime(StringUtils.parseTime(time));
|
||||
} catch (Exception e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (altitude != null) {
|
||||
try {
|
||||
@@ -492,28 +502,22 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
private void insertLocation(TrackPoint trackPoint) {
|
||||
if (trackData.trackStatisticsUpdater == null) {
|
||||
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoint.getTime() != -1L ? trackPoint.getTime() : trackData.importTime);
|
||||
}
|
||||
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
|
||||
protected void insertTrackPoint(TrackPoint trackPoint) {
|
||||
trackData.bufferedTrackPoints.add(trackPoint);
|
||||
trackData.numBufferedTrackPoints++;
|
||||
trackData.numberOfLocations++;
|
||||
}
|
||||
|
||||
protected boolean isFirstTrackPointInSegment() {
|
||||
return trackData.lastLocationInCurrentSegment == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the locations to the database.
|
||||
*
|
||||
* @param data the track data
|
||||
* Flushes the TrackPoints to the database.
|
||||
*/
|
||||
private void flushLocations(TrackData data) {
|
||||
if (data.numBufferedTrackPoints <= 0) {
|
||||
return;
|
||||
private void flushTrackPoints() {
|
||||
if (trackData.bufferedTrackPoints.size() > 0) {
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
|
||||
trackData.bufferedTrackPoints.clear();
|
||||
}
|
||||
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.track.getId());
|
||||
data.numBufferedTrackPoints = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,7 +532,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
// TODO Why private inner class?
|
||||
// TODO Why private inner class?
|
||||
private static class TrackData {
|
||||
// The current track
|
||||
final Track track = new Track();
|
||||
@@ -539,19 +543,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
// The last location in the current segment; Null if the current segment doesn't have a last location
|
||||
TrackPoint lastLocationInCurrentSegment;
|
||||
|
||||
// The number of locations processed for the current track
|
||||
int numberOfLocations = 0;
|
||||
|
||||
// The TrackStatisticsUpdater for the current track
|
||||
TrackStatisticsUpdater trackStatisticsUpdater;
|
||||
|
||||
// The import time of the track.
|
||||
final long importTime = System.currentTimeMillis();
|
||||
|
||||
// The buffered locations
|
||||
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
|
||||
|
||||
// The number of buffered locations
|
||||
int numBufferedTrackPoints = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
/**
|
||||
* Imports a GPX file.
|
||||
* Uses https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
|
||||
* <p>
|
||||
* {@link de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter} does not export information if a segment was started automatic or manually.
|
||||
* Therefore, all segments starts are marked as SEGMENT_START_AUTOMATIC.
|
||||
* Thus, the {@link de.dennisguse.opentracks.stats.TrackStatistics} cannot be restored correctly.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@@ -101,6 +105,9 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
case TAG_TRACK:
|
||||
onTrackEnd();
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_TRACK_POINT:
|
||||
onTrackPointEnd();
|
||||
break;
|
||||
@@ -193,13 +200,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
loss = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point end.
|
||||
*/
|
||||
private void onTrackPointEnd() throws SAXException {
|
||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
||||
TrackPoint trackPoint = getTrackPoint();
|
||||
if (trackPoint == null) {
|
||||
return;
|
||||
if (isFirstTrackPointInSegment) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
insertTrackPoint(trackPoint);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
@@ -39,7 +39,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
|
||||
|
||||
private static final String MARKER_STYLE = "#" + KmlTrackWriter.MARKER_STYLE;
|
||||
private static final String MARKER_STYLE = "#" + KMLTrackExporter.MARKER_STYLE;
|
||||
|
||||
private static final String TAG_COORDINATES = "coordinates";
|
||||
private static final String TAG_DESCRIPTION = "description";
|
||||
@@ -159,6 +159,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (content != null) {
|
||||
time = content.trim();
|
||||
}
|
||||
|
||||
break;
|
||||
case TAG_STYLE_URL:
|
||||
if (content != null) {
|
||||
@@ -221,10 +222,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
elevationLossList.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* On track segment end.
|
||||
*/
|
||||
private void onTrackSegmentEnd() {
|
||||
protected void onTrackSegmentEnd() {
|
||||
super.onTrackSegmentEnd();
|
||||
// Close a track segment by inserting the segment locations
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
@@ -256,24 +255,32 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
* On track point end. gx:coord end tag.
|
||||
*/
|
||||
private void onTrackPointEnd() throws SAXException {
|
||||
// Add location to locationList
|
||||
// Add trackPoint to trackPoints
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
String[] parts = content.trim().split(" ");
|
||||
if (parts.length != 2 && parts.length != 3) {
|
||||
return;
|
||||
if (parts.length == 2 || parts.length == 3) {
|
||||
longitude = parts[0];
|
||||
latitude = parts[1];
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
}
|
||||
longitude = parts[0];
|
||||
latitude = parts[1];
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
|
||||
TrackPoint location = getTrackPoint();
|
||||
if (location == null) {
|
||||
return;
|
||||
// Similar to GPX
|
||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
||||
TrackPoint trackPoint = getTrackPoint();
|
||||
if (isFirstTrackPointInSegment) {
|
||||
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
|
||||
|
||||
trackPoint.setType(type);
|
||||
}
|
||||
trackPoints.add(location);
|
||||
trackPoints.add(trackPoint);
|
||||
|
||||
// Reset variables for next trackpoint (which might not have such data).
|
||||
time = null;
|
||||
longitude = null;
|
||||
latitude = null;
|
||||
altitude = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,36 +294,34 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
* On extended data value end. gx:value end tag.
|
||||
*/
|
||||
private void onExtendedDataValueEnd() throws SAXException {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
content = content.trim();
|
||||
if (content.equals("")) {
|
||||
return;
|
||||
}
|
||||
float value;
|
||||
try {
|
||||
value = Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
content = content.trim();
|
||||
if (!content.equals("")) {
|
||||
try {
|
||||
value = Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (extendedDataType) {
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
||||
speedList.add(value);
|
||||
break;
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_POWER:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
|
||||
powerList.add(value);
|
||||
break;
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_HEART_RATE:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
|
||||
heartRateList.add(value);
|
||||
break;
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_CADENCE:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
|
||||
cadenceList.add(value);
|
||||
break;
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_ELEVATION_GAIN:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ELEVATION_GAIN:
|
||||
elevationGainList.add(value);
|
||||
break;
|
||||
case KmlTrackWriter.EXTENDED_DATA_TYPE_ELEVATION_LOSS:
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ELEVATION_LOSS:
|
||||
elevationLossList.add(value);
|
||||
break;
|
||||
default:
|
||||
|
||||