Import: re-implement GPX parsing.

This commit is contained in:
Dennis Guse
2021-05-27 17:49:19 +02:00
parent 54e8fd3ee3
commit 288fe27a57
7 changed files with 225 additions and 594 deletions
@@ -204,6 +204,7 @@ public class TrackPoint {
this.longitude = longitude;
}
//TODO Better return null, if no location is present aka latitude == null etc.
@NonNull
public Location getLocation() {
Location location = new Location("");
@@ -336,10 +337,12 @@ public class TrackPoint {
return hasAccuracy() && accuracy < poorAccuracy;
}
//TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull TrackPoint dest) {
return getLocation().bearingTo(dest.getLocation());
}
//TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull Location dest) {
return getLocation().bearingTo(dest);
}
@@ -1,509 +0,0 @@
/*
* Copyright 2012 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.importer;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Altitude;
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.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* Abstract class for file track importers.
*
* @author Jimmy Shih
*/
abstract class AbstractFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private final Distance recordingDistanceInterval;
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
// The current element content
//TODO Should be made private and getter be used by child classes.
protected String content;
protected String icon;
protected String name;
protected String description;
protected String category;
protected String latitude;
protected String longitude;
protected String altitude;
protected String time;
protected String speed;
protected String heartrate;
protected String cadence;
protected String power;
protected String markerType;
protected String photoUrl;
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
// The current track data
private TrackData trackData;
// The SAX locator to get the current line information
private Locator locator;
private final SharedPreferences sharedPreferences;
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
this.context = context;
this.contentProviderUtils = contentProviderUtils;
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void characters(char[] ch, int start, int length) {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
// In 99% of the cases, a single call to this method will be made for each sequence of characters we're interested in, so we'll rarely be concatenating strings, thus not justifying the use of a StringBuilder.
content += newContent;
}
}
protected void onFileEnd() {
// Add markers to the last imported track
int size = trackIds.size();
if (size == 0) {
return;
}
Track.Id trackId = trackIds.get(size - 1);
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
return;
}
int markerPosition = -1;
Marker marker = null;
TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (true) {
if (marker == null) {
markerPosition++;
marker = markerPosition < markers.size() ? markers.get(markerPosition) : null;
if (marker == null) {
// No more markers
return;
}
// If marker had photo it must be translated to internal photo url (depend on track id)
if (marker.hasPhoto()) {
marker.setPhotoUrl(getInternalPhotoUrl(marker.getPhotoUrl()));
}
}
if (trackPoint == null) {
if (!trackPointIterator.hasNext()) {
// No more track points. Ignore the rest of the markers.
return;
}
trackPoint = trackPointIterator.next();
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if ((marker.getTime()).isAfter(trackPoint.getTime())) {
trackPoint = null;
} else if (marker.getTime().isBefore(trackPoint.getTime())) {
Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
marker = null;
} else {
// The marker trackPoint time matches the track point time
if (!trackPoint.hasLocation()) {
// Invalid trackPoint, load the next trackPoint
trackPoint = null;
continue;
}
// Valid trackPoint
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_icon_url);
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
contentProviderUtils.insertMarker(newMarker);
}
// Load the next marker
marker = null;
}
}
}
}
/**
* On track start.
*/
protected void onTrackStart() throws SAXException {
trackData = new TrackData();
}
/**
* On track end.
*/
protected void onTrackEnd() {
if (name != null) {
trackData.track.setName(name);
}
try {
trackData.track.setUuid(UUID.fromString(uuid));
} catch (IllegalArgumentException | NullPointerException e) {
Log.w(TAG, "could not parse Track UUID, generating a new one.");
trackData.track.setUuid(UUID.randomUUID());
}
if (description != null) {
trackData.track.setDescription(description);
}
if (category != null) {
trackData.track.setCategory(category);
//TODO remove when GPX and KML support reading this property.
if (icon == null)
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
}
if (icon != null) {
trackData.track.setIcon(icon);
}
TrackStatisticsUpdater statistics = new TrackStatisticsUpdater();
//TODO I guess, we should not filter by recordingDistanceInterval on import; the data is already recorded, so we should not change it.
for (TrackPoint trackPoint : trackData.bufferedTrackPoints) {
statistics.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if (!statistics.isTrackInitialized()) {
throw new ImportParserException("Track did not contain any locations.");
}
trackData.track.setTrackStatistics(statistics.getTrackStatistics());
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
if (track != null) {
if (PreferencesUtils.getPreventReimportTracks(sharedPreferences, context)) {
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
}
//TODO This is a workaround until we have proper UI.
trackData.track.setUuid(UUID.randomUUID());
}
if (trackIds.size() > 0) {
// TODO Multi track is not supported yet.
cleanImport();
throw new ImportParserException("Multi track not supported");
}
Uri uri = contentProviderUtils.insertTrack(trackData.track);
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
trackIds.add(trackId);
trackData.track.setId(trackId);
flushTrackPoints();
}
protected void onTrackSegmentStart() {
trackData.numberOfSegments++;
//If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
if (trackData.numberOfSegments > 1
&& trackData.lastLocationInCurrentSegment != null
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL))
) {
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
protected void onTrackSegmentEnd() {
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
if (trackPoint == null) {
return;
}
if (!trackPoint.hasLocation()) {
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
}
}
protected void addMarker() throws ParsingException {
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time ignored.");
return;
}
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
return;
}
Marker marker = new Marker(null, trackPoint);
if (name != null) {
marker.setName(name);
}
if (description != null) {
marker.setDescription(description);
}
if (category != null) {
marker.setCategory(category);
}
if (photoUrl != null) {
marker.setPhotoUrl(photoUrl);
}
markers.add(marker);
}
protected TrackPoint getTrackPoint() throws ParsingException {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint.hasLocation()) {
Instant time = trackPoint.getTime();
if (trackPoint.getLatitude() == 100) {
//TODO Remove by 31st December 2021.
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
trackPoint.setTime(time);
} else if (trackPoint.getLatitude() == 200) {
//TODO Remove by 31st December 2021.
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
trackPoint.setTime(time);
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
}
}
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() == null) {
if (!trackPoint.hasSpeed()) {
Duration timeDifference = Duration.between(trackData.lastLocationInCurrentSegment.getTime(), trackPoint.getTime());
// Check for negative time change
if (timeDifference.isNegative()) {
Log.w(TAG, "Time difference not positive.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the last two points.
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
}
}
}
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
}
trackData.lastLocationInCurrentSegment = trackPoint;
return trackPoint;
}
/**
* Creates an error message.
*
* @param message the message
*/
protected String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
}
/**
* Gets the photo url for a file.
*
* @param externalPhotoUrl the file name
*/
protected String getInternalPhotoUrl(String externalPhotoUrl) {
if (trackData.track.getId() == null) {
Log.e(TAG, "Track id is invalid.");
return null;
}
if (externalPhotoUrl == null) {
Log.i(TAG, "External photo url is null.");
return null;
}
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
File file = FileUtils.buildInternalPhotoFile(context, trackData.track.getId(), Uri.parse(importFileName));
if (file != null) {
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
return null;
}
protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
return trackPoint;
}
try {
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
if (altitude != null) {
try {
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
return trackPoint;
}
/**
* Inserts a trackPoint.
*
* @param trackPoint the trackPoint
*/
protected void insertTrackPoint(TrackPoint trackPoint) {
trackData.bufferedTrackPoints.add(trackPoint);
}
protected boolean isFirstTrackPointInSegment() {
return trackData.lastLocationInCurrentSegment == null;
}
/**
* Flushes the TrackPoints to the database.
*/
private void flushTrackPoints() {
if (trackData.bufferedTrackPoints.size() > 0) {
contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
trackData.bufferedTrackPoints.clear();
}
}
@Override
public List<Track.Id> getImportTrackIds() {
return trackIds;
}
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
/**
* Data for the current track.
*
* @author Jimmy Shih
*/
// TODO Why private inner class?
private static class TrackData {
// The current track
final Track track = new Track();
// The number of segments processed for the current track
int numberOfSegments = 0;
// The last location in the current segment; Null if the current segment doesn't have a last location
TrackPoint lastLocationInCurrentSegment;
// The buffered locations
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
}
public static class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
}
@@ -17,19 +17,30 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.SharedPreferences;
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.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Altitude;
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.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Imports a GPX file.
@@ -43,7 +54,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
*
* @author Jimmy Shih
*/
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
public class GpxFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
private static final String TAG = GpxFileTrackImporter.class.getSimpleName();
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
@@ -70,23 +83,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
/**
* Constructor.
*
* @param context the context
*/
private Locator locator;
private final Context context;
// Belongs to the current track
private final ArrayList<Marker> markers = new ArrayList<>();
// The current element content
private String content = "";
private String name;
private String description;
private String category;
private String latitude;
private String longitude;
private String altitude;
private String time;
private String speed;
private String heartrate;
private String cadence;
private String power;
protected String markerType;
protected String photoUrl;
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
private final TrackImporter trackImporter;
public GpxFileTrackImporter(Context context) {
this(context, new ContentProviderUtils(context));
}
@VisibleForTesting
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
this.context = context;
//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, contentProviderUtils, recordingDistanceInterval, maxRecordingDistance, preventReimport);
}
@Override
public DefaultHandler getHandler() {
return this;
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
@@ -96,7 +143,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
onMarkerStart(attributes);
break;
case TAG_TRACK:
onTrackStart();
trackImporter.newTrack();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
@@ -107,6 +154,11 @@ public class GpxFileTrackImporter 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) {
switch (tag) {
@@ -117,13 +169,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
onMarkerEnd();
break;
case TAG_TRACK:
onTrackEnd();
trackImporter.setTrack(context, name, uuid, description, category, null);
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT:
onTrackPointEnd();
currentSegment.add(createTrackPoint());
break;
case TAG_NAME:
if (content != null) {
@@ -197,33 +249,65 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
break;
}
// Reset element content
content = null;
content = "";
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
private void onTrackSegmentStart() {
}
@Override
protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = super.createTrackPoint();
private void onTrackSegmentEnd() {
if (currentSegment.isEmpty()) {
Log.w(TAG, "No locations in current segment.");
return;
}
TrackPoint first = currentSegment.getFirst();
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackImporter.addTrackPoints(currentSegment);
currentSegment.clear();
}
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
return trackPoint;
}
try {
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
if (altitude != null) {
try {
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
if (speed != null) {
try {
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
@@ -239,7 +323,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
@@ -247,21 +331,21 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Distance.of(distance));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
@@ -269,11 +353,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
return trackPoint;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
@@ -285,15 +364,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
loss = null;
}
private void onTrackPointEnd() {
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
TrackPoint trackPoint = getTrackPoint();
if (isFirstTrackPointInSegment) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
insertTrackPoint(trackPoint);
}
private void onMarkerStart(Attributes attributes) {
name = null;
description = null;
@@ -307,6 +377,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
}
private void onMarkerEnd() {
addMarker();
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time; ignored.");
return;
}
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
return;
}
Marker marker = new Marker(null, trackPoint);
if (name != null) {
marker.setName(name);
}
if (description != null) {
marker.setDescription(description);
}
if (category != null) {
marker.setCategory(category);
}
if (photoUrl != null) {
marker.setPhotoUrl(photoUrl);
}
markers.add(marker);
}
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 onFileEnd() {
trackImporter.addMarkers(markers);
trackImporter.finish();
}
@Override
public DefaultHandler getHandler() {
return this;
}
@Override
public List<Track.Id> getImportTrackIds() {
return trackImporter.getTrackIds();
}
@Override
public void cleanImport() {
trackImporter.cleanImport();
}
}
@@ -30,7 +30,6 @@ 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;
@@ -41,7 +40,6 @@ 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.
@@ -120,6 +118,11 @@ public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.
this.trackImporter = new TrackImporter(context, new ContentProviderUtils(context), recordingDistanceInterval, maxRecordingDistance, preventReimport);
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
@@ -152,7 +155,7 @@ public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
case TAG_KML:
onFileEnded();
onFileEnd();
break;
case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY:
@@ -163,7 +166,7 @@ public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.
onMarkerLocationEnd();
break;
case TAG_GX_MULTI_TRACK:
onTrackEnd();
trackImporter.setTrack(context, name, uuid, description, category, icon);
break;
case TAG_GX_TRACK:
onTrackSegmentEnd();
@@ -362,14 +365,14 @@ public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.
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);
throw new 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);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
}
@@ -420,38 +423,11 @@ public class KmlFileTrackImporter extends DefaultHandler implements XMLImporter.
}
}
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() {
private void onFileEnd() {
trackImporter.addMarkers(markers);
trackImporter.finish();
}
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.io.file.importer;
import androidx.annotation.NonNull;
public class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
@@ -28,6 +28,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* Handles logic to import:
@@ -93,8 +94,28 @@ public class TrackImporter {
this.markers.addAll(markers);
}
void setTrack(Track track) {
this.track = track;
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
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) {
icon = TrackIconUtils.getIconValue(context, category);
}
}
track.setIcon(icon != null ? icon : "");
}
void finish() {
@@ -155,12 +176,11 @@ public class TrackImporter {
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);
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, time));
} else if (current.getLatitude() == 200) {
//TODO Remove by 31st December 2021.
current = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
current.setTime(time);
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time));
//TODO Delete location
} else if (!LocationUtils.isValidLocation(current.getLocation())) {
throw new ImportParserException("Invalid location detected: " + current);
}
@@ -45,7 +45,7 @@ public class XMLImporter {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
return parser.getImportTrackIds();
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
} catch (SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (parser.getImportTrackIds().size() > 0) {
parser.cleanImport();