Add CSV as export format (TrackPoints only).

Fixes #999.
This commit is contained in:
Dennis Guse
2022-04-19 18:57:28 +02:00
parent 3815ee6c59
commit 374959f340
6 changed files with 273 additions and 3 deletions
@@ -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());
+11
View File
@@ -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",,,,,,,,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal speed altitude_gain altitude_loss sensor_distance heartrate cadence power
2 2020-02-02T03:02:02+01:00 SEGMENT_START_MANUAL
3 2020-02-02T03:02:03+01:00 TRACKPOINT 3 3 10 10 54 1 1
4 2020-02-02T03:02:04+01:00 SENSORPOINT 54 1 1 10 66 3 50
5 2020-02-02T03:02:05+01:00 TRACKPOINT 3 3 10 10 18 0 0 2 66 3 50
6 2020-02-02T03:02:06+01:00 SEGMENT_END_MANUAL
7 2020-02-02T03:02:20+01:00 SEGMENT_START_MANUAL
8 2020-02-02T03:02:21+01:00 TRACKPOINT 3 3 10 10 54 0 0
9 2020-02-02T03:02:22+01:00 SEGMENT_START_AUTOMATIC 3 3 10 10 54 0 0
10 2020-02-02T03:02:23+01:00 TRACKPOINT 3 3 10 10 54 0 0
11 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];