mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-11 13:33:07 +02:00
Merging GPX import fix
This commit is contained in:
@@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|||||||
import com.google.android.apps.mytracks.content.Track;
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.StringUtils;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
@@ -28,11 +29,8 @@ import android.util.Log;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SimpleTimeZone;
|
|
||||||
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import javax.xml.parsers.SAXParser;
|
import javax.xml.parsers.SAXParser;
|
||||||
@@ -54,22 +52,6 @@ import org.xml.sax.helpers.DefaultHandler;
|
|||||||
*/
|
*/
|
||||||
public class GpxImporter extends DefaultHandler {
|
public class GpxImporter extends DefaultHandler {
|
||||||
|
|
||||||
/*
|
|
||||||
* Different date formats used in GPX files
|
|
||||||
*/
|
|
||||||
static final SimpleDateFormat DATE_FORMAT1 = new SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ssZ");
|
|
||||||
static final SimpleDateFormat DATE_FORMAT2 = new SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ss'Z'");
|
|
||||||
static final SimpleDateFormat DATE_FORMAT3 = new SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
|
||||||
static final SimpleTimeZone UTC_TIMEZONE = new SimpleTimeZone(0, "UTC");
|
|
||||||
static {
|
|
||||||
DATE_FORMAT1.setTimeZone(UTC_TIMEZONE);
|
|
||||||
DATE_FORMAT2.setTimeZone(UTC_TIMEZONE);
|
|
||||||
DATE_FORMAT3.setTimeZone(UTC_TIMEZONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* GPX-XML tag names and attributes.
|
* GPX-XML tag names and attributes.
|
||||||
*/
|
*/
|
||||||
@@ -211,7 +193,7 @@ public class GpxImporter extends DefaultHandler {
|
|||||||
this.providerUtils = providerUtils;
|
this.providerUtils = providerUtils;
|
||||||
tracksWritten = new ArrayList<Long>();
|
tracksWritten = new ArrayList<Long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void characters(char[] ch, int start, int length) throws SAXException {
|
public void characters(char[] ch, int start, int length) throws SAXException {
|
||||||
String newContent = new String(ch, start, length);
|
String newContent = new String(ch, start, length);
|
||||||
@@ -453,7 +435,16 @@ public class GpxImporter extends DefaultHandler {
|
|||||||
private void onTimeElementEnd() throws SAXException {
|
private void onTimeElementEnd() throws SAXException {
|
||||||
if (location == null) { return; }
|
if (location == null) { return; }
|
||||||
|
|
||||||
long time = parseTimeForAllFormats(content);
|
// Parse the time
|
||||||
|
long time;
|
||||||
|
try {
|
||||||
|
time = StringUtils.parseXmlDateTime(content.trim());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
String msg = createErrorMessage("Unable to parse time: " + content);
|
||||||
|
throw new SAXException(msg, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate derived attributes from previous point
|
||||||
if (lastSegmentLocation != null) {
|
if (lastSegmentLocation != null) {
|
||||||
long timeDifference = time - lastSegmentLocation.getTime();
|
long timeDifference = time - lastSegmentLocation.getTime();
|
||||||
|
|
||||||
@@ -473,7 +464,9 @@ public class GpxImporter extends DefaultHandler {
|
|||||||
location.setBearing(lastSegmentLocation.bearingTo(location));
|
location.setBearing(lastSegmentLocation.bearingTo(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in the time
|
||||||
location.setTime(time);
|
location.setTime(time);
|
||||||
|
|
||||||
// initialize start time with time of first track point
|
// initialize start time with time of first track point
|
||||||
if (statsBuilder == null) {
|
if (statsBuilder == null) {
|
||||||
statsBuilder = new TripStatisticsBuilder();
|
statsBuilder = new TripStatisticsBuilder();
|
||||||
@@ -511,44 +504,6 @@ public class GpxImporter extends DefaultHandler {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse time trying different formats used in GPX files.
|
|
||||||
*
|
|
||||||
* @param timeContents string with time information
|
|
||||||
* @return time as long
|
|
||||||
* @throws SAXException on time parsing errors
|
|
||||||
*/
|
|
||||||
private long parseTimeForAllFormats(String timeContents) throws SAXException {
|
|
||||||
long time;
|
|
||||||
timeContents = timeContents.trim();
|
|
||||||
|
|
||||||
// 1st try with time zone at end a la "+0000"
|
|
||||||
time = parseTime(timeContents, DATE_FORMAT1);
|
|
||||||
if (time > -1) { return time; }
|
|
||||||
|
|
||||||
// if that fails, try with a literal "Z" at the end
|
|
||||||
// (this is not according to xml standard, but some gpx files are like
|
|
||||||
// that):
|
|
||||||
time = parseTime(timeContents, DATE_FORMAT2);
|
|
||||||
if (time > -1) { return time; }
|
|
||||||
|
|
||||||
// some gpx timestamps have 3 additional digits at the end.
|
|
||||||
time = parseTime(timeContents, DATE_FORMAT3);
|
|
||||||
if (time > -1) { return time; }
|
|
||||||
|
|
||||||
// everything failed - abort the import
|
|
||||||
String msg = createErrorMessage("Invalid time format: " + timeContents);
|
|
||||||
throw new SAXException(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseTime(String timeContents, SimpleDateFormat format) {
|
|
||||||
try {
|
|
||||||
return format.parse(timeContents).getTime();
|
|
||||||
} catch (ParseException ex) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ID of the last point inserted into the database.
|
* Returns the ID of the last point inserted into the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,17 +16,22 @@
|
|||||||
package com.google.android.apps.mytracks.util;
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||||
|
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import java.text.ParsePosition;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.SimpleTimeZone;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Various string manipulation methods.
|
* Various string manipulation methods.
|
||||||
@@ -76,6 +81,75 @@ public class StringUtils implements DescriptionGenerator {
|
|||||||
return "<![CDATA[" + escaped + "]]>";
|
return "<![CDATA[" + escaped + "]]>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final SimpleDateFormat BASE_XML_DATE_FORMAT =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||||
|
static {
|
||||||
|
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
|
||||||
|
}
|
||||||
|
private static final Pattern XML_DATE_EXTRAS_PATTERN =
|
||||||
|
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an XML dateTime element as defined by
|
||||||
|
* {@link http://www.w3.org/TR/xmlschema-2/#dateTime}.
|
||||||
|
*/
|
||||||
|
public static long parseXmlDateTime(String xmlTime) {
|
||||||
|
// Parse the base date (fixed format)
|
||||||
|
ParsePosition position = new ParsePosition(0);
|
||||||
|
Date date = BASE_XML_DATE_FORMAT.parse(xmlTime, position);
|
||||||
|
if (date == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid XML dateTime value: '" + xmlTime
|
||||||
|
+ "' (at position " + position.getErrorIndex() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the extras
|
||||||
|
Matcher matcher =
|
||||||
|
XML_DATE_EXTRAS_PATTERN.matcher(xmlTime.substring(position.getIndex()));
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
// This will match even an empty string as all groups are optional,
|
||||||
|
// so a non-match means some other garbage was there
|
||||||
|
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
long time = date.getTime();
|
||||||
|
|
||||||
|
// Account for fractional seconds
|
||||||
|
String fractional = matcher.group(1);
|
||||||
|
if (fractional != null) {
|
||||||
|
// Regex ensures fractional part is in (0,1(
|
||||||
|
float fractionalSeconds = Float.parseFloat(fractional);
|
||||||
|
long fractionalMillis = (long) (fractionalSeconds * 1000.0f);
|
||||||
|
time += fractionalMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for timezones
|
||||||
|
String sign = matcher.group(2);
|
||||||
|
String offsetHoursStr = matcher.group(3);
|
||||||
|
String offsetMinsStr = matcher.group(4);
|
||||||
|
if (sign != null && offsetHoursStr != null && offsetMinsStr != null) {
|
||||||
|
// Regex ensures sign is + or -
|
||||||
|
boolean plusSign = sign.equals("+");
|
||||||
|
int offsetHours = Integer.parseInt(offsetHoursStr);
|
||||||
|
int offsetMins = Integer.parseInt(offsetMinsStr);
|
||||||
|
|
||||||
|
// Regex ensures values are >= 0
|
||||||
|
if (offsetHours > 14 || offsetMins > 59) {
|
||||||
|
throw new IllegalArgumentException("Bad timezone in " + xmlTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalOffsetMillis = (offsetMins + offsetHours * 60L) * 60000L;
|
||||||
|
|
||||||
|
// Make time go back to UTC
|
||||||
|
if (plusSign) {
|
||||||
|
time -= totalOffsetMillis;
|
||||||
|
} else {
|
||||||
|
time += totalOffsetMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a number of milliseconds as a string.
|
* Formats a number of milliseconds as a string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ public class GpxImporterTest extends AndroidTestCase {
|
|||||||
private static final String TRACK_LAT_2 = "48.768374";
|
private static final String TRACK_LAT_2 = "48.768374";
|
||||||
private static final String TRACK_LON_2 = "9.177816";
|
private static final String TRACK_LON_2 = "9.177816";
|
||||||
private static final String TRACK_ELE_2 = "333.0";
|
private static final String TRACK_ELE_2 = "333.0";
|
||||||
private static final String TRACK_TIME_2 = "2010-04-22T18:21:50Z";
|
private static final String TRACK_TIME_2 = "2010-04-22T18:21:50.12";
|
||||||
|
|
||||||
|
private static final SimpleDateFormat DATE_FORMAT1 =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||||
|
private static final SimpleDateFormat DATE_FORMAT2 =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
|
||||||
|
|
||||||
// TODO use real files from different sources with more track points
|
// TODO use real files from different sources with more track points
|
||||||
private static final String VALID_TEST_GPX = "<gpx><trk><name><![CDATA["
|
private static final String VALID_TEST_GPX = "<gpx><trk><name><![CDATA["
|
||||||
@@ -114,15 +119,14 @@ public class GpxImporterTest extends AndroidTestCase {
|
|||||||
public void testImportSuccess() throws Exception {
|
public void testImportSuccess() throws Exception {
|
||||||
Capture<Track> trackParam = new Capture<Track>();
|
Capture<Track> trackParam = new Capture<Track>();
|
||||||
|
|
||||||
SimpleDateFormat format = GpxImporter.DATE_FORMAT2;
|
|
||||||
Location loc1 = new Location(LocationManager.GPS_PROVIDER);
|
Location loc1 = new Location(LocationManager.GPS_PROVIDER);
|
||||||
loc1.setTime(format.parse(TRACK_TIME_1).getTime());
|
loc1.setTime(DATE_FORMAT2.parse(TRACK_TIME_1).getTime());
|
||||||
loc1.setLatitude(Double.parseDouble(TRACK_LAT_1));
|
loc1.setLatitude(Double.parseDouble(TRACK_LAT_1));
|
||||||
loc1.setLongitude(Double.parseDouble(TRACK_LON_1));
|
loc1.setLongitude(Double.parseDouble(TRACK_LON_1));
|
||||||
loc1.setAltitude(Double.parseDouble(TRACK_ELE_1));
|
loc1.setAltitude(Double.parseDouble(TRACK_ELE_1));
|
||||||
|
|
||||||
Location loc2 = new Location(LocationManager.GPS_PROVIDER);
|
Location loc2 = new Location(LocationManager.GPS_PROVIDER);
|
||||||
loc2.setTime(format.parse(TRACK_TIME_2).getTime());
|
loc2.setTime(DATE_FORMAT1.parse(TRACK_TIME_2).getTime());
|
||||||
loc2.setLatitude(Double.parseDouble(TRACK_LAT_2));
|
loc2.setLatitude(Double.parseDouble(TRACK_LAT_2));
|
||||||
loc2.setLongitude(Double.parseDouble(TRACK_LON_2));
|
loc2.setLongitude(Double.parseDouble(TRACK_LON_2));
|
||||||
loc2.setAltitude(Double.parseDouble(TRACK_ELE_2));
|
loc2.setAltitude(Double.parseDouble(TRACK_ELE_2));
|
||||||
@@ -152,7 +156,7 @@ public class GpxImporterTest extends AndroidTestCase {
|
|||||||
Track track = trackParam.getValue();
|
Track track = trackParam.getValue();
|
||||||
assertEquals(TRACK_NAME, track.getName());
|
assertEquals(TRACK_NAME, track.getName());
|
||||||
assertEquals(TRACK_DESC, track.getDescription());
|
assertEquals(TRACK_DESC, track.getDescription());
|
||||||
assertEquals(format.parse(TRACK_TIME_1).getTime(), track.getStatistics()
|
assertEquals(DATE_FORMAT2.parse(TRACK_TIME_1).getTime(), track.getStatistics()
|
||||||
.getStartTime());
|
.getStartTime());
|
||||||
assertNotSame(-1, track.getStartId());
|
assertNotSame(-1, track.getStartId());
|
||||||
assertNotSame(-1, track.getStopId());
|
assertNotSame(-1, track.getStopId());
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link StringUtils}.
|
||||||
|
*
|
||||||
|
* @author Rodrigo Damazio
|
||||||
|
*/
|
||||||
|
public class StringUtilsTest extends TestCase {
|
||||||
|
public void testParseXmlDateTime() {
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01",
|
||||||
|
2010, 5, 4, 3, 2, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseXmlDateTime_fractional() {
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.3",
|
||||||
|
2010, 5, 4, 3, 2, 1, 300);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.35",
|
||||||
|
2010, 5, 4, 3, 2, 1, 350);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.352",
|
||||||
|
2010, 5, 4, 3, 2, 1, 352);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.3525",
|
||||||
|
2010, 5, 4, 3, 2, 1, 352);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseXmlDateTime_timezone() {
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01Z",
|
||||||
|
2010, 5, 4, 3, 2, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01+00:00",
|
||||||
|
2010, 5, 4, 3, 2, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01-00:00",
|
||||||
|
2010, 5, 4, 3, 2, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01+01:00",
|
||||||
|
2010, 5, 4, 2, 2, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01+10:30",
|
||||||
|
2010, 5, 3, 16, 32, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01-09:30",
|
||||||
|
2010, 5, 4, 12, 32, 1, 0);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01-05:00",
|
||||||
|
2010, 5, 4, 8, 2, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseXmlDateTime_fractionalAndTimezone() {
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.352Z",
|
||||||
|
2010, 5, 4, 3, 2, 1, 352);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.47+00:00",
|
||||||
|
2010, 5, 4, 3, 2, 1, 470);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.5791+03:00",
|
||||||
|
2010, 5, 4, 0, 2, 1, 579);
|
||||||
|
assertParseXmlDateTime("2010-05-04T03:02:01.8-05:30",
|
||||||
|
2010, 5, 4, 8, 32, 1, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertParseXmlDateTime(String dateTime,
|
||||||
|
int year, int month, int day, int hour, int min, int second, int millis) {
|
||||||
|
long timestamp = StringUtils.parseXmlDateTime(dateTime);
|
||||||
|
GregorianCalendar calendar =
|
||||||
|
new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||||
|
calendar.set(year, month - 1, day, hour, min, second);
|
||||||
|
calendar.set(GregorianCalendar.MILLISECOND, millis);
|
||||||
|
assertEquals(calendar.getTimeInMillis(), timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user