From 288fe27a5756cc670c899eaba46abe80bbf650c1 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Thu, 27 May 2021 17:49:19 +0200 Subject: [PATCH] Import: re-implement GPX parsing. --- .../opentracks/content/data/TrackPoint.java | 3 + .../importer/AbstractFileTrackImporter.java | 509 ------------------ .../file/importer/GpxFileTrackImporter.java | 209 +++++-- .../file/importer/KmlFileTrackImporter.java | 44 +- .../io/file/importer/ParsingException.java | 20 + .../io/file/importer/TrackImporter.java | 32 +- .../io/file/importer/XMLImporter.java | 2 +- 7 files changed, 225 insertions(+), 594 deletions(-) delete mode 100644 src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java create mode 100644 src/main/java/de/dennisguse/opentracks/io/file/importer/ParsingException.java 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 568fe60c9..0d4a55f89 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -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); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java deleted file mode 100644 index da7d036b2..000000000 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java +++ /dev/null @@ -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 trackIds = new ArrayList<>(); - private final List 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 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 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(); - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java index 22c042981..46c22b1e6 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java @@ -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 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 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 getImportTrackIds() { + return trackImporter.getTrackIds(); + } + + @Override + public void cleanImport() { + trackImporter.cleanImport(); } } 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 a7ecb1eab..a42be0536 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 @@ -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(); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ParsingException.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ParsingException.java new file mode 100644 index 000000000..b679f2d55 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/ParsingException.java @@ -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(); + } +} 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 index 87d0aeb86..82b42e493 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/TrackImporter.java @@ -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); } 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 08dbea330..76b9b46ed 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 @@ -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();