Merging GPX import fix

This commit is contained in:
Rodrigo Damazio
2010-09-07 15:08:54 -03:00
4 changed files with 180 additions and 65 deletions
@@ -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.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
import android.location.LocationManager;
@@ -28,11 +29,8 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.SimpleTimeZone;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
@@ -54,22 +52,6 @@ import org.xml.sax.helpers.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.
*/
@@ -211,7 +193,7 @@ public class GpxImporter extends DefaultHandler {
this.providerUtils = providerUtils;
tracksWritten = new ArrayList<Long>();
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String newContent = new String(ch, start, length);
@@ -453,7 +435,16 @@ public class GpxImporter extends DefaultHandler {
private void onTimeElementEnd() throws SAXException {
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) {
long timeDifference = time - lastSegmentLocation.getTime();
@@ -473,7 +464,9 @@ public class GpxImporter extends DefaultHandler {
location.setBearing(lastSegmentLocation.bearingTo(location));
}
// Fill in the time
location.setTime(time);
// initialize start time with time of first track point
if (statsBuilder == null) {
statsBuilder = new TripStatisticsBuilder();
@@ -511,44 +504,6 @@ public class GpxImporter extends DefaultHandler {
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.
*/
@@ -16,17 +16,22 @@
package com.google.android.apps.mytracks.util;
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.Waypoint;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Various string manipulation methods.
@@ -76,6 +81,75 @@ public class StringUtils implements DescriptionGenerator {
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.
*
@@ -62,7 +62,12 @@ public class GpxImporterTest extends AndroidTestCase {
private static final String TRACK_LAT_2 = "48.768374";
private static final String TRACK_LON_2 = "9.177816";
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
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 {
Capture<Track> trackParam = new Capture<Track>();
SimpleDateFormat format = GpxImporter.DATE_FORMAT2;
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.setLongitude(Double.parseDouble(TRACK_LON_1));
loc1.setAltitude(Double.parseDouble(TRACK_ELE_1));
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.setLongitude(Double.parseDouble(TRACK_LON_2));
loc2.setAltitude(Double.parseDouble(TRACK_ELE_2));
@@ -152,7 +156,7 @@ public class GpxImporterTest extends AndroidTestCase {
Track track = trackParam.getValue();
assertEquals(TRACK_NAME, track.getName());
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());
assertNotSame(-1, track.getStartId());
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);
}
}