forked from upstream-mirrors/OpenTracks
@@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
@@ -27,15 +28,19 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
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.TestDataUtil;
|
||||
@@ -426,6 +431,36 @@ public class ExportImportTest {
|
||||
assertNull(trackImported);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void csv_export_only() throws TimeoutException, IOException {
|
||||
setUp();
|
||||
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// then
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
|
||||
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
|
||||
InputStream actual = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
String actualText = new BufferedReader(new InputStreamReader(actual, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
assertEquals(expectedText, actualText);
|
||||
}
|
||||
|
||||
private void assertMarkers() {
|
||||
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#time,trackpoint_type,latitude,longitude,altitude,accuracy_horizontal,speed,altitude_gain,altitude_loss,sensor_distance,heartrate,cadence,power
|
||||
"2020-02-02T03:02:02+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,
|
||||
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,3,10,10,54,1,1,,,,
|
||||
"2020-02-02T03:02:04+01:00","SENSORPOINT",,,,,54,1,1,10,66,3,50
|
||||
"2020-02-02T03:02:05+01:00","TRACKPOINT",3,3,10,10,18,0,0,2,66,3,50
|
||||
"2020-02-02T03:02:06+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,
|
||||
"2020-02-02T03:02:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,
|
||||
"2020-02-02T03:02:21+01:00","TRACKPOINT",3,3,10,10,54,0,0,,,,
|
||||
"2020-02-02T03:02:22+01:00","SEGMENT_START_AUTOMATIC",3,3,10,10,54,0,0,,,,
|
||||
"2020-02-02T03:02:23+01:00","TRACKPOINT",3,3,10,10,54,0,0,,,,
|
||||
"2020-02-02T03:02:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,
|
||||
|
@@ -53,8 +53,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6;
|
||||
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
|
||||
|
||||
private static final int URI_SHARE_PICTURE = 8;
|
||||
private static final int URI_CSV = 8;
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
private static final String TRACKID_DELIMITER = "_";
|
||||
@@ -66,6 +65,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getName() + "/*/*", URI_CSV);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,6 +146,9 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
|
||||
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
|
||||
|
||||
case URI_CSV:
|
||||
return TrackFileFormat.CSV;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.CSVTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
||||
@@ -100,6 +101,23 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "gpx";
|
||||
}
|
||||
},
|
||||
|
||||
CSV {
|
||||
@Override
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new CSVTrackExporter(new ContentProviderUtils(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "text/csv";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return "csv";
|
||||
}
|
||||
};
|
||||
|
||||
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Exports the {@link TrackPoint} into a CSV.
|
||||
* * decimal separator: .
|
||||
* * column separator: ,
|
||||
* <p>
|
||||
* NOTE:
|
||||
* * {@link Track} data is not exported.
|
||||
* * {@link Marker} data is not exported.
|
||||
*/
|
||||
public class CSVTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = CSVTrackExporter.class.getSimpleName();
|
||||
|
||||
private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
|
||||
static {
|
||||
ALTITUDE_FORMAT.setMaximumFractionDigits(1);
|
||||
ALTITUDE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
COORDINATE_FORMAT.setMaximumFractionDigits(6);
|
||||
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
|
||||
COORDINATE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
SPEED_FORMAT.setMaximumFractionDigits(2);
|
||||
SPEED_FORMAT.setGroupingUsed(false);
|
||||
|
||||
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
|
||||
HEARTRATE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
CADENCE_FORMAT.setMaximumFractionDigits(0);
|
||||
CADENCE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
POWER_FORMAT.setMaximumFractionDigits(0);
|
||||
POWER_FORMAT.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private PrintWriter printWriter;
|
||||
|
||||
public CSVTrackExporter(ContentProviderUtils contentProviderUtils) {
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
List<Column> columns = Collections.unmodifiableList(Arrays.asList(
|
||||
new Column("time", null),
|
||||
new Column("trackpoint_type", t -> quote(t.getType().name())),
|
||||
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""),
|
||||
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
|
||||
|
||||
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
|
||||
new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
|
||||
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""),
|
||||
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
|
||||
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
|
||||
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
|
||||
new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")
|
||||
));
|
||||
|
||||
try {
|
||||
prepare(outputStream);
|
||||
|
||||
boolean headerWritten = false;
|
||||
|
||||
for (Track track : tracks) {
|
||||
columns.get(0).extractor = t -> quote(StringUtils.formatDateTimeIso8601(t.getTime(), track.getZoneOffset()));
|
||||
|
||||
if (!headerWritten) {
|
||||
writeHeader(columns);
|
||||
headerWritten = true;
|
||||
}
|
||||
|
||||
writeTrackPoints(columns, track);
|
||||
}
|
||||
|
||||
close();
|
||||
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Thread interrupted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTrackPoints(List<Column> columns, Track track) throws InterruptedException {
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
case SEGMENT_START_MANUAL:
|
||||
case SEGMENT_END_MANUAL:
|
||||
case SEGMENT_START_AUTOMATIC:
|
||||
case SENSORPOINT:
|
||||
case TRACKPOINT:
|
||||
writeTrackPoint(columns, trackPoint);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void writeHeader(List<Column> columns) {
|
||||
if (printWriter != null) {
|
||||
String columnNames = columns.stream().map(c -> c.columnName)
|
||||
.reduce((s, s2) -> s + "," + s2)
|
||||
.orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println("#" + columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
|
||||
if (printWriter != null) {
|
||||
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint))
|
||||
.reduce((s, s2) -> s + "," + s2)
|
||||
.orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println(columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Column {
|
||||
final String columnName;
|
||||
Function<TrackPoint, String> extractor;
|
||||
|
||||
Column(String columnName, Function<TrackPoint, String> extractor) {
|
||||
this.columnName = columnName;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
}
|
||||
|
||||
private static String quote(String content) {
|
||||
return '"' + content + '"';
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.GPX
|
||||
TrackFileFormat.GPX,
|
||||
TrackFileFormat.CSV
|
||||
};
|
||||
String[] entries = new String[trackFileFormats.length];
|
||||
String[] entryValues = new String[trackFileFormats.length];
|
||||
|
||||
Reference in New Issue
Block a user