GPX: only export TrackPoints with lat/lng. (#591)

GPX: only export TrackPoints with lat/lng.
Thus, segmentation (manual/automatic) cannot be restored on import.
This also affects the TrackStatistics.
This commit is contained in:
Dennis Guse
2021-02-09 19:00:28 +01:00
committed by GitHub
parent 4a340380ac
commit fa12086951
14 changed files with 379 additions and 467 deletions
@@ -36,7 +36,7 @@ import java.util.Objects;
* 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 problems, we need to store both timestamps.
* TODO: if these might be a problem, we need to store both timestamps.
*
* @author Sandor Dornbush
*/
@@ -177,7 +177,7 @@ public class TrackPoint {
return latitude != null || longitude != null;
}
@Nullable
@NonNull
public Location getLocation() {
Location location = new Location("");
location.setTime(time.toEpochMilli());
@@ -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())) {
@@ -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);
@@ -1,190 +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 java.time.Duration;
import java.time.Instant;
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;
/**
* 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();
}
//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) {
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, -1)) {
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, 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) {
trackWriter.writeBeginTrack(track, trackPoint);
wroteTrack = true;
}
boolean newSegment = TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(trackPoint.getType()) || TrackPoint.Type.SEGMENT_START_MANUAL.equals(trackPoint.getType());
if (newSegment) {
if (wroteSegment) trackWriter.writeCloseSegment();
trackWriter.writeOpenSegment();
trackWriter.writeTrackPoint(trackPoint);
wroteSegment = true;
continue;
}
if (TrackPoint.Type.SEGMENT_END_MANUAL.equals(trackPoint.getType())) {
if (!wroteSegment) trackWriter.writeOpenSegment();
trackWriter.writeTrackPoint(trackPoint);
trackWriter.writeCloseSegment();
wroteSegment = false;
continue;
}
trackWriter.writeTrackPoint(trackPoint);
}
if (wroteSegment) {
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
//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
*/
//TODO Why?
private void setLocationTime(TrackPoint trackPoint, Duration offset) {
if (trackPoint != null) {
trackPoint.setTime(trackPoint.getTime().minus(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,6 +16,11 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.database.Cursor;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
@@ -24,15 +29,21 @@ 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 KML.
* NOTE:
* * does not export {@link TrackPoint} without a latitude/longitude (not supported by GPX 1.1).
*
* @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);
@@ -62,19 +73,106 @@ 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) {
try {
prepare(outputStream);
writeHeader(track);
writeMarkers(track);
writeLocations(track);
writeFooter();
close();
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Thread interrupted", e);
return false;
}
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
throw new UnsupportedOperationException();
}
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();
@@ -82,8 +180,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");
@@ -103,31 +201,34 @@ 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) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
@@ -142,17 +243,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>");
@@ -167,28 +257,24 @@ 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) {
String coordinates = trackPoint.hasLocation() ? " " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) : "";
printWriter.println("<trkpt " + coordinates + ">");
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
@@ -208,7 +294,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()) {
@@ -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,13 +13,20 @@
* 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;
@@ -31,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";
@@ -76,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;
@@ -91,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\"");
@@ -141,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) {
@@ -160,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) {
@@ -180,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>");
@@ -188,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());
@@ -218,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>");
@@ -233,8 +350,8 @@ public class KmlTrackWriter implements TrackWriter {
}
}
@Override
public void writeOpenSegment() {
@VisibleForTesting
void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
speedList.clear();
@@ -246,8 +363,8 @@ public class KmlTrackWriter implements TrackWriter {
}
}
@Override
public void writeCloseSegment() {
@VisibleForTesting
void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
@@ -277,8 +394,8 @@ 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>");
@@ -351,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) {
@@ -474,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);
}
@@ -36,7 +36,6 @@ 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;
@@ -121,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);
@@ -375,7 +370,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / timeDifference.toMillis();
trackPoint.setSpeed((float) speed);
trackPoint.setSpeed(speed);
}
}
}
@@ -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
*/
@@ -200,9 +204,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
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);
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";
@@ -306,22 +306,22 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
}
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:
@@ -35,7 +35,7 @@ public class ExportUtils {
}
public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) {
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
TrackExporter trackExporter = trackFileFormat.createTrackExporter(context);
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track.getId(), trackFileFormat, directory);
if (exportDocumentFileUri == null) {