From 54e8fd3ee37d21e745e7a0c1acb36447c8c566f4 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Tue, 25 May 2021 18:40:52 +0200 Subject: [PATCH] Import: re-implement KML parsing. Fixes #790. --- .../io/file/importer/ExportImportTest.java | 20 +- .../opentracks/content/data/Marker.java | 25 +- .../opentracks/content/data/Track.java | 1 + .../opentracks/content/data/TrackPoint.java | 22 +- .../provider/ContentProviderUtils.java | 11 + .../file/importer/KmlFileTrackImporter.java | 294 +++++++++++++----- .../io/file/importer/TrackImporter.java | 267 ++++++++++++++++ .../io/file/importer/XMLImporter.java | 1 + .../stats/TrackStatisticsUpdater.java | 10 +- 9 files changed, 540 insertions(+), 111 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index e8ca6204e..f8c97f590 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -406,18 +406,18 @@ public class ExportImportTest { // TODO Not exported for GPX/KML // assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01); - assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation()); + assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation()); if (trackPoint.hasLocation()) { - assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); - assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); + assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); + assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); } - assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed()); + assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.hasSpeed()); if (trackPoint.hasSpeed()) { - assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001); + assertEquals("" + i, trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001); } - assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude()); + assertEquals("" + i, trackPoint.hasAltitude(), importedTrackPoint.hasAltitude()); if (trackPoint.hasAltitude()) { - assertEquals(trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001); + assertEquals("" + i, trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001); } if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) { @@ -426,13 +426,13 @@ public class ExportImportTest { } if (verifyHeartrate) { - assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01); + assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01); } if (verifyCadence) { - assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01); + assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01); } if (verifyPower) { - assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01); + assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01); } if (verifyAltitudeGain) { assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01); diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Marker.java b/src/main/java/de/dennisguse/opentracks/content/data/Marker.java index 9b2054566..0306d0fab 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Marker.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Marker.java @@ -44,7 +44,7 @@ public final class Marker { private String description = ""; private String category = ""; private String icon = ""; - private final Track.Id trackId; + private Track.Id trackId; private final Instant time; private Double latitude; @@ -74,16 +74,13 @@ public final class Marker { 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 = trackPoint.getAltitude(); - if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing(); + setTrackPoint(trackPoint); this.length = Distance.of(0); //TODO Not cool! this.duration = Duration.ofMillis(0); //TODO Not cool! } + @Deprecated public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) { this(trackId, trackPoint); this.name = name; @@ -95,6 +92,15 @@ public final class Marker { this.photoUrl = photoUrl; } + //TODO Is somehow part of the initialization process. Can we at least limit visibility? + public void setTrackPoint(TrackPoint trackPoint) { + this.latitude = trackPoint.getLatitude(); + this.longitude = trackPoint.getLongitude(); + if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy(); + if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude(); + if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing(); + } + /** * May be null if the it was not loaded from the database. */ @@ -143,16 +149,19 @@ public final class Marker { this.icon = icon; } - @NonNull public Track.Id getTrackId() { return trackId; } + @Deprecated + public void setTrackId(@NonNull Track.Id trackId) { + this.trackId = trackId; + } + public boolean hasLocation() { return latitude != null || longitude != null; } - @Nullable public Location getLocation() { Location location = new Location(""); location.setTime(time.toEpochMilli()); diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Track.java b/src/main/java/de/dennisguse/opentracks/content/data/Track.java index 4b48676df..c3e86c35f 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Track.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Track.java @@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics; * @author Leif Hendrik Wilden * @author Rodrigo Damazio */ +//TODO Do not default initialize attributes; might be confusing for debugging public class Track { private Track.Id id; diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index c3cd10448..568fe60c9 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -55,7 +55,7 @@ public class TrackPoint { private Altitude altitude; private Speed speed; private Float bearing; - private Distance sensorDistance_m; + private Distance sensorDistance; public enum Type { SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume) @@ -99,7 +99,11 @@ public class TrackPoint { } public TrackPoint(@NonNull Location location) { - this(Type.TRACKPOINT); + this(Type.TRACKPOINT, location, Instant.now()); + } + + public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) { + this(type); this.latitude = location.getLatitude(); this.longitude = location.getLongitude(); @@ -107,7 +111,9 @@ public class TrackPoint { this.speed = Speed.of(location.getSpeed()); this.accuracy = location.getAccuracy(); - setTime(Instant.now()); + //TODO Should we copy the bearing? + + setTime(time); } public TrackPoint(@NonNull Type type, Instant time) { @@ -314,10 +320,14 @@ public class TrackPoint { this.accuracy = horizontalAccuracy; } + @Nullable public Distance distanceToPrevious(@NonNull TrackPoint previous) { if (hasSensorDistance()) { return getSensorDistance(); } + if (!(hasLocation() && previous.hasLocation())) { + return null; + } return Distance.of(getLocation().distanceTo(previous.getLocation())); } @@ -336,15 +346,15 @@ public class TrackPoint { // Sensor data public boolean hasSensorDistance() { - return sensorDistance_m != null; + return sensorDistance != null; } public Distance getSensorDistance() { - return sensorDistance_m; + return sensorDistance; } public void setSensorDistance(Distance distance_m) { - this.sensorDistance_m = distance_m; + this.sensorDistance = distance_m; } public boolean hasSensorData() { diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 0d01696e6..0b6e85578 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -243,6 +243,7 @@ public class ContentProviderUtils { * @param track the track * @return the content provider URI of the inserted track. */ + ///TODO Return Track.Id public Uri insertTrack(Track track) { return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track)); } @@ -569,6 +570,7 @@ public class ContentProviderUtils { } //TODO Only used for file import; might be better to replace it. + //TODO Rename to bulkInsert public int bulkInsertTrackPoint(List trackPoints, Track.Id trackId) { ContentValues[] values = new ContentValues[trackPoints.size()]; for (int i = 0; i < trackPoints.size(); i++) { @@ -577,6 +579,15 @@ public class ContentProviderUtils { return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values); } + //TODO Set trackId in this method. + public int bulkInsertMarkers(List markers, Track.Id trackId) { + ContentValues[] values = new ContentValues[markers.size()]; + for (int i = 0; i < markers.size(); i++) { + values[i] = createContentValues(markers.get(i)); + } + return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values); + } + /** * Gets the last location id for a track. * Returns -1L if it doesn't exist. diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java index 527df20a6..a7ecb1eab 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java @@ -17,28 +17,38 @@ package de.dennisguse.opentracks.io.file.importer; import android.content.Context; +import android.content.SharedPreferences; +import android.location.Location; import android.util.Log; -import androidx.annotation.VisibleForTesting; - import org.xml.sax.Attributes; +import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; +import java.time.Instant; import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Speed; +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.exporter.KMLTrackExporter; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.util.StringUtils; +import de.dennisguse.opentracks.util.TrackIconUtils; /** * Imports a KML file. * * @author Jimmy Shih */ -public class KmlFileTrackImporter extends AbstractFileTrackImporter { +public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser { private static final String TAG = KmlFileTrackImporter.class.getSimpleName(); @@ -64,29 +74,50 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { private static final String ATTRIBUTE_NAME = "name"; - private boolean trackStarted = false; + private Locator locator; + + private final Context context; + + // Belongs to the current track + private final ArrayList whenList = new ArrayList<>(); + private final ArrayList locationList = new ArrayList<>(); + private String extendedDataType; - private final ArrayList trackPoints = new ArrayList<>(); - private final ArrayList speedList = new ArrayList<>(); - private final ArrayList distanceList = new ArrayList<>(); - private final ArrayList cadenceList = new ArrayList<>(); - private final ArrayList heartRateList = new ArrayList<>(); - private final ArrayList powerList = new ArrayList<>(); + private final ArrayList sensorSpeedList = new ArrayList<>(); + private final ArrayList sensorDistanceList = new ArrayList<>(); + private final ArrayList sensorCadenceList = new ArrayList<>(); + private final ArrayList sensorHeartRateList = new ArrayList<>(); + private final ArrayList sensorPowerList = new ArrayList<>(); private final ArrayList altitudeGainList = new ArrayList<>(); private final ArrayList altitudeLossList = new ArrayList<>(); + private final ArrayList markers = new ArrayList<>(); + + // The current element content + private String content = ""; + + private String icon; + private String name; + private String description; + private String category; + private String latitude; + private String longitude; + private String altitude; + private String markerType; + private String photoUrl; + private String uuid; + + private final TrackImporter trackImporter; + public KmlFileTrackImporter(Context context) { - this(context, new ContentProviderUtils(context)); - } + this.context = context; - @VisibleForTesting - KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) { - super(context, contentProviderUtils); - } - - @Override - public DefaultHandler getHandler() { - return this; + //TODO move this to instantiation of this class + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); + Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); + boolean preventReimport = PreferencesUtils.getPreventReimportTracks(sharedPreferences, context); + this.trackImporter = new TrackImporter(context, new ContentProviderUtils(context), recordingDistanceInterval, maxRecordingDistance, preventReimport); } @Override @@ -98,11 +129,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { onMarkerStart(); break; case TAG_GX_MULTI_TRACK: - trackStarted = true; - onTrackStart(); + trackImporter.newTrack(); break; case TAG_GX_TRACK: - if (!trackStarted) { + if (trackImporter == null) { throw new SAXException("No " + TAG_GX_MULTI_TRACK); } onTrackSegmentStart(); @@ -113,11 +143,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } } + @Override + public void characters(char[] ch, int start, int length) { + content += new String(ch, start, length); + } + @Override public void endElement(String uri, String localName, String tag) throws SAXException { switch (tag) { case TAG_KML: - onFileEnd(); + onFileEnded(); break; case TAG_PLACEMARK: case TAG_PHOTO_OVERLAY: @@ -134,7 +169,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { onTrackSegmentEnd(); break; case TAG_GX_COORD: - onTrackPointEnd(); + onCoordEnded(); break; case TAG_GX_VALUE: onExtendedDataValueEnd(); @@ -166,7 +201,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { break; case TAG_WHEN: if (content != null) { - time = content.trim(); + whenList.add(StringUtils.parseTime(content.trim())); } break; @@ -183,7 +218,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } // Reset element content - content = null; + content = ""; } private void onMarkerStart() { @@ -196,7 +231,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { latitude = null; longitude = null; altitude = null; - time = null; markerType = null; } @@ -204,7 +238,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { if (!MARKER_STYLE.equals(markerType)) { return; } - addMarker(); + + if (whenList.size() != 1) { + Log.w(TAG, "Marker without time ignored."); + return; + } + + Location location = createLocation(longitude, latitude, altitude); + if (location == null) { + Log.w(TAG, "Marker with invalid coordinates ignored: " + location); + return; + } + + Marker marker = new Marker(null, new TrackPoint(TrackPoint.Type.TRACKPOINT, location, whenList.get(0))); //TODO Creating marker without need + marker.setName(name != null ? name : ""); + marker.setDescription(description != null ? description : ""); + marker.setCategory(category != null ? category : ""); + marker.setPhotoUrl(photoUrl); + markers.add(marker); + + name = null; + description = null; + category = null; + photoUrl = null; + whenList.clear(); } private void onMarkerLocationEnd() { @@ -219,39 +276,56 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } } - @Override - protected void onTrackSegmentStart() { - super.onTrackSegmentStart(); - trackPoints.clear(); - speedList.clear(); - distanceList.clear(); - heartRateList.clear(); - cadenceList.clear(); - powerList.clear(); + private void onTrackSegmentStart() { + locationList.clear(); + whenList.clear(); + + sensorSpeedList.clear(); + sensorDistanceList.clear(); + sensorHeartRateList.clear(); + sensorCadenceList.clear(); + sensorPowerList.clear(); altitudeGainList.clear(); altitudeLossList.clear(); } - 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); + private void onTrackSegmentEnd() { + if (locationList.size() != whenList.size()) { + throw new ImportParserException(" and should have the same count."); + } - if (i < speedList.size() && speedList.get(i) != null) { - trackPoint.setSpeed(Speed.of(speedList.get(i))); + // Close a track segment by inserting the segment locations + for (int i = 0; i < locationList.size(); i++) { + Instant time = whenList.get(i); + Location location = locationList.get(i); + + TrackPoint trackPoint; + if (i == 0) { + if (location == null) { + trackPoint = TrackPoint.createSegmentStartManualWithTime(time); + } else { + trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time); + } + } else if (i == locationList.size() - 1 && location == null) { + trackPoint = TrackPoint.createSegmentEndWithTime(time); + } else { + trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time); } - if (i < distanceList.size() && distanceList.get(i) != null) { - trackPoint.setSensorDistance(Distance.of(distanceList.get(i))); + + if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) { + trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i))); } - if (i < heartRateList.size()) { - trackPoint.setHeartRate_bpm(heartRateList.get(i)); + if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) { + trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i))); } - if (i < cadenceList.size()) { - trackPoint.setCyclingCadence_rpm(cadenceList.get(i)); + if (i < sensorHeartRateList.size()) { + trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i)); } - if (i < powerList.size()) { - trackPoint.setPower(powerList.get(i)); + if (i < sensorCadenceList.size()) { + trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i)); + } + if (i < sensorPowerList.size()) { + trackPoint.setPower(sensorPowerList.get(i)); } if (i < altitudeGainList.size()) { trackPoint.setAltitudeGain(altitudeGainList.get(i)); @@ -260,18 +334,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { trackPoint.setAltitudeLoss(altitudeLossList.get(i)); } - insertTrackPoint(trackPoint); + trackImporter.addTrackPoint(trackPoint); } } - /** - * On track point end. gx:coord end tag. - */ - private void onTrackPointEnd() { - // Add trackPoint to trackPoints - if (content == null) { - return; - } + private void onCoordEnded() { String[] parts = content.trim().split(" "); if (parts.length == 2 || parts.length == 3) { longitude = parts[0]; @@ -279,33 +346,41 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { altitude = parts.length == 3 ? parts[2] : null; } - // 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; + locationList.add(createLocation(longitude, latitude, altitude)); - trackPoint.setType(type); - } - trackPoints.add(trackPoint); - - // Reset variables for next trackpoint (which might not have such data). - time = null; longitude = null; latitude = null; altitude = null; } - /** - * On extended data start. gx:SimpleArrayData start tag. - */ + private Location createLocation(String longitude, String latitude, String altitude) { + Location location = null; + if (longitude != null || latitude != null) { + location = new Location("import"); + + try { + location.setLatitude(Double.parseDouble(latitude)); + location.setLongitude(Double.parseDouble(longitude)); + } catch (NumberFormatException e) { + throw new AbstractFileTrackImporter.ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e); + } + + if (altitude != null) { + try { + location.setAltitude(Double.parseDouble(altitude)); + } catch (NumberFormatException e) { + throw new AbstractFileTrackImporter.ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e); + } + } + } + return location; + } + + private void onExtendedDataStart(Attributes attributes) { extendedDataType = attributes.getValue(ATTRIBUTE_NAME); } - /** - * On extended data value end. gx:value end tag. - */ private void onExtendedDataValueEnd() throws SAXException { Float value = null; if (content != null) { @@ -320,19 +395,19 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } switch (extendedDataType) { case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED: - speedList.add(value); + sensorSpeedList.add(value); break; case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE: - distanceList.add(value); + sensorDistanceList.add(value); break; case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER: - powerList.add(value); + sensorPowerList.add(value); break; case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE: - heartRateList.add(value); + sensorHeartRateList.add(value); break; case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE: - cadenceList.add(value); + sensorCadenceList.add(value); break; case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN: altitudeGainList.add(value); @@ -344,4 +419,55 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported."); } } + + private void onTrackEnd() { + Track track = new Track(); + track.setName(name != null ? name : ""); + + try { + track.setUuid(UUID.fromString(uuid)); + } catch (IllegalArgumentException | NullPointerException e) { + Log.w(TAG, "could not parse Track UUID, generating a new one."); + track.setUuid(UUID.randomUUID()); + } + + track.setDescription(description != null ? description : ""); + + if (category != null) { + track.setCategory(category); + + if (icon == null) { + //TODO exporting/importing icon is not implemented. + icon = TrackIconUtils.getIconValue(context, category); + } + } + + track.setIcon(icon != null ? icon : ""); + + trackImporter.setTrack(track); + } + + private String createErrorMessage(String message) { + return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message); + } + + private void onFileEnded() { + trackImporter.addMarkers(markers); + trackImporter.finish(); + } + + @Override + public DefaultHandler getHandler() { + return this; + } + + @Override + public List getImportTrackIds() { + return trackImporter.getTrackIds(); + } + + @Override + public void cleanImport() { + trackImporter.cleanImport(); + } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java new file mode 100644 index 000000000..87d0aeb86 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java @@ -0,0 +1,267 @@ +package de.dennisguse.opentracks.io.file.importer; + +import android.content.ContentUris; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Marker; +import de.dennisguse.opentracks.content.data.Speed; +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.stats.TrackStatistics; +import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; +import de.dennisguse.opentracks.util.FileUtils; +import de.dennisguse.opentracks.util.LocationUtils; + +/** + * Handles logic to import: + * 1. addTrackPoints() + * 2. addMarkers(); + * 3. setTrack(); + * 4. newTrack(); //stores current track to databse + * 5. if needed go to 1. + * 6. finish() + *

+ * NOTE: This class modifies the parameter. + * Do not re-use these objects anywhere else. + */ +public class TrackImporter { + + private static final String TAG = TrackImporter.class.getSimpleName(); + + private final Context context; + private final ContentProviderUtils contentProviderUtils; + + private final Distance recordingDistanceInterval; + private final Distance maxRecordingDistance; + private final boolean preventReimport; + + private final List trackIds = new ArrayList<>(); + + // Current track + private Track track; + private final List trackPoints = new LinkedList<>(); + private final List markers = new LinkedList<>(); + + public TrackImporter(Context context, ContentProviderUtils contentProviderUtils, Distance recordingDistanceInterval, Distance maxRecordingDistance, boolean preventReimport) { + this.context = context; + this.contentProviderUtils = contentProviderUtils; + this.recordingDistanceInterval = recordingDistanceInterval; + this.maxRecordingDistance = maxRecordingDistance; + this.preventReimport = preventReimport; + } + + void newTrack() { + if (track != null) { + finishTrack(); + } + + track = null; + trackPoints.clear(); + markers.clear(); + } + + void addTrackPoint(TrackPoint trackPoint) { + this.trackPoints.add(trackPoint); + } + + void addTrackPoints(List trackPoints) { + this.trackPoints.addAll(trackPoints); + } + + void addMarker(Marker marker) { + this.markers.add(marker); + } + + void addMarkers(List markers) { + this.markers.addAll(markers); + } + + void setTrack(Track track) { + this.track = track; + } + + void finish() { + if (track != null) { + finishTrack(); + } + } + + private void finishTrack() { + if (trackPoints.isEmpty()) { + throw new ImportParserException("Cannot import track without any locations."); + } + + // Store Track + if (contentProviderUtils.getTrack(track.getUuid()) != null) { + if (preventReimport) { + throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport)); + } + + //TODO This is a workaround until we have proper UI. + track.setUuid(UUID.randomUUID()); + } + + TrackStatisticsUpdater updater = new TrackStatisticsUpdater(); + updater.addTrackPoints(trackPoints, recordingDistanceInterval); + track.setTrackStatistics(updater.getTrackStatistics()); + + Uri trackUri = contentProviderUtils.insertTrack(track); + Track.Id trackId = new Track.Id(ContentUris.parseId(trackUri)); + + // Store TrackPoints + adjustTrackPoints(); + contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId); + + // Store Markers + matchMarkers2TrackPoints(trackId); + for (Marker marker : markers) + marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers + + contentProviderUtils.bulkInsertMarkers(markers, trackId); + + //Clear up. + trackPoints.clear(); + markers.clear(); + + trackIds.add(trackId); + } + + /** + * If not present: calculate data from the previous trackPoint (if present) + * NOTE: Modifies content of trackPoints. + */ + private void adjustTrackPoints() { + for (int i = 0; i < trackPoints.size(); i++) { + TrackPoint current = trackPoints.get(i); + + if (current.hasLocation()) { + Instant time = current.getTime(); + if (current.getLatitude() == 100) { + //TODO Remove by 31st December 2021. + current = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL); + current.setTime(time); + } else if (current.getLatitude() == 200) { + //TODO Remove by 31st December 2021. + current = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL); + current.setTime(time); + } else if (!LocationUtils.isValidLocation(current.getLocation())) { + throw new ImportParserException("Invalid location detected: " + current); + } + } + } + + for (int i = 1; i < trackPoints.size(); i++) { + TrackPoint previous = trackPoints.get(i - 1); + TrackPoint current = trackPoints.get(i); + + Distance distanceToPrevious = current.distanceToPrevious(previous); + if (distanceToPrevious != null) { + if (!current.hasSpeed()) { + Duration timeDifference = Duration.between(previous.getTime(), current.getTime()); + current.setSpeed(Speed.of(distanceToPrevious, timeDifference)); + } + + if (!current.hasBearing()) { + current.setBearing(previous.bearingTo(current)); + } + + if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) { + current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC); + } + } + } + } + + /** + * NOTE: Modifies content of markers (incl. removal). + */ + private void matchMarkers2TrackPoints(Track.Id trackId) { + List trackPointsWithLocation = trackPoints.stream() + .filter(TrackPoint::hasLocation) + .collect(Collectors.toList()); + + List todoMarkers = new LinkedList<>(markers); + List doneMarkers = new LinkedList<>(); + + for (final TrackPoint trackPoint : trackPointsWithLocation) { + if (todoMarkers.isEmpty()) { + break; + } + + TrackStatisticsUpdater updater = new TrackStatisticsUpdater(); + updater.addTrackPoint(trackPoint, recordingDistanceInterval); + + List matchedMarkers = todoMarkers.stream() + .filter(it -> trackPoint.getLatitude() == it.getLatitude() + && trackPoint.getLongitude() == it.getLongitude() + && trackPoint.getTime().equals(it.getTime()) + ) + .collect(Collectors.toList()); + + TrackStatistics statistics = updater.getTrackStatistics(); + for (Marker marker : matchedMarkers) { + if (marker.hasPhoto()) { + marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl())); + } + + marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why? + + marker.setLength(statistics.getTotalDistance()); + marker.setDuration(statistics.getTotalTime()); + + marker.setTrackPoint(trackPoint); + } + + todoMarkers.removeAll(matchedMarkers); + doneMarkers.addAll(matchedMarkers); + } + + if (todoMarkers.isEmpty()) { + Log.w(TAG, "Some markers could not be attached to TrackPoints; those are not imported."); + } + + markers.clear(); + markers.addAll(doneMarkers); + } + + /** + * Gets the photo url for a file. + * + * @param externalPhotoUrl the file name + */ + private String getInternalPhotoUrl(@NonNull Track.Id trackId, @NonNull String externalPhotoUrl) { + String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl); + File file = FileUtils.buildInternalPhotoFile(context, trackId, Uri.parse(importFileName)); + if (file != null) { + Uri photoUri = FileUtils.getUriForFile(context, file); + return "" + photoUri; + } + + return null; + } + + public List getTrackIds() { + return Collections.unmodifiableList(trackIds); + } + + public void cleanImport() { + contentProviderUtils.deleteTracks(context, trackIds); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/XMLImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/XMLImporter.java index 8d60d88b6..08dbea330 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/XMLImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/XMLImporter.java @@ -58,6 +58,7 @@ public class XMLImporter { } interface TrackParser { + @Deprecated DefaultHandler getHandler(); List getImportTrackIds(); diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index 886b1cc71..65927cc41 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import java.time.Duration; +import java.util.List; import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Speed; @@ -98,10 +99,13 @@ public class TrackStatisticsUpdater { return trackInitialized; } + public void addTrackPoints(List trackPoints, Distance minGPSDistance) { + for (TrackPoint tp : trackPoints) { + addTrackPoint(tp, minGPSDistance); + } + } + /** - * Adds a trackPoint. - * - * @param trackPoint the trackPoint * @param minGPSDistance the min recording distance */ public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {