mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-02 17:43:06 +02:00
Timezone support: store timezone offset per Track.
When a recording is started the offset of the phone is used. Timezone is exported to KML and GPX. Fixes #301.
This commit is contained in:
+7
-3
@@ -1,5 +1,7 @@
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@@ -10,12 +12,11 @@ import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class KmlTrackExporterTest {
|
||||
|
||||
@@ -27,6 +28,8 @@ public class KmlTrackExporterTest {
|
||||
@Test
|
||||
public void writeCloseSegment_only_write_sensordata_if_present() {
|
||||
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<when>1970-01-01T01:00:00+01:00</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<ExtendedData>\n" +
|
||||
"<SchemaData schemaUrl=\"#schema\">\n" +
|
||||
@@ -41,7 +44,8 @@ public class KmlTrackExporterTest {
|
||||
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
kmlTrackWriter.prepare(outputStream);
|
||||
|
||||
kmlTrackWriter.writeTrackPoint(trackPoint);
|
||||
kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint);
|
||||
kmlTrackWriter.writeTrackPoint(ZoneOffset.ofTotalSeconds(3600), trackPoint);
|
||||
|
||||
// when
|
||||
kmlTrackWriter.writeCloseSegment();
|
||||
|
||||
@@ -219,6 +219,7 @@ public class ExportImportTest {
|
||||
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
|
||||
|
||||
// Time
|
||||
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
|
||||
|
||||
@@ -338,6 +339,7 @@ public class ExportImportTest {
|
||||
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
|
||||
|
||||
// Time
|
||||
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@@ -31,9 +34,6 @@ import java.util.TimeZone;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests for {@link StringUtils}.
|
||||
*
|
||||
@@ -141,7 +141,7 @@ public class StringUtilsTest {
|
||||
|
||||
// This comparision tends to be flaky (difference of 1ms)
|
||||
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
|
||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toEpochMilli()) <= 1);
|
||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toInstant().toEpochMilli()) <= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,7 +21,10 @@ import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -45,8 +48,19 @@ public class Track {
|
||||
|
||||
private String icon = "";
|
||||
|
||||
private final ZoneOffset zoneOffset;
|
||||
|
||||
private TrackStatistics trackStatistics = new TrackStatistics();
|
||||
|
||||
@VisibleForTesting
|
||||
public Track() {
|
||||
this.zoneOffset = ZoneOffset.UTC;
|
||||
}
|
||||
|
||||
public Track(@NonNull ZoneOffset zoneOffset) {
|
||||
this.zoneOffset = zoneOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the track was not loaded from the database.
|
||||
*/
|
||||
@@ -99,6 +113,14 @@ public class Track {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public ZoneOffset getZoneOffset() {
|
||||
return zoneOffset;
|
||||
}
|
||||
|
||||
public OffsetDateTime getStartTime() {
|
||||
return trackStatistics.getStartTime().atOffset(zoneOffset);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
return trackStatistics;
|
||||
|
||||
@@ -41,6 +41,7 @@ public interface TracksColumns extends BaseColumns {
|
||||
String DESCRIPTION = "description"; // track description
|
||||
String CATEGORY = "category"; // track activity type
|
||||
String STARTTIME = "starttime"; // track start time
|
||||
String STARTTIME_OFFSET = "starttime_offset"; // in plus/minus in seconds
|
||||
String STOPTIME = "stoptime"; // track stop time
|
||||
String MARKER_COUNT = "markerCount"; // the numbers of markers (virtual column)
|
||||
@Deprecated
|
||||
@@ -77,7 +78,8 @@ public interface TracksColumns extends BaseColumns {
|
||||
+ ALTITUDE_GAIN + " FLOAT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ UUID + " BLOB, "
|
||||
+ ALTITUDE_LOSS + " FLOAT)";
|
||||
+ ALTITUDE_LOSS + " FLOAT, "
|
||||
+ STARTTIME_OFFSET + " INTEGER)";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
@@ -98,6 +99,7 @@ public class ContentProviderUtils {
|
||||
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
|
||||
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
|
||||
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
|
||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
@@ -109,7 +111,7 @@ public class ContentProviderUtils {
|
||||
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ALTITUDE_LOSS);
|
||||
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
|
||||
|
||||
Track track = new Track();
|
||||
Track track = new Track(ZoneOffset.ofTotalSeconds(cursor.getInt(startTimeOffsetIndex)));
|
||||
TrackStatistics trackStatistics = track.getTrackStatistics();
|
||||
if (!cursor.isNull(idIndex)) {
|
||||
track.setId(new Track.Id(cursor.getLong(idIndex)));
|
||||
@@ -126,6 +128,7 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(categoryIndex)) {
|
||||
track.setCategory(cursor.getString(categoryIndex));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(startTimeIndex)) {
|
||||
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
|
||||
}
|
||||
@@ -288,6 +291,7 @@ public class ContentProviderUtils {
|
||||
values.put(TracksColumns.NAME, track.getName());
|
||||
values.put(TracksColumns.DESCRIPTION, track.getDescription());
|
||||
values.put(TracksColumns.CATEGORY, track.getCategory());
|
||||
values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds());
|
||||
if (trackStatistics.getStartTime() != null) {
|
||||
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
|
||||
}
|
||||
|
||||
+56
-1
@@ -9,6 +9,9 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.zone.ZoneRules;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.Startup;
|
||||
@@ -26,7 +29,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 32;
|
||||
private static final int DATABASE_VERSION = 33;
|
||||
|
||||
public CustomSQLiteOpenHelper(Context context) {
|
||||
this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
|
||||
@@ -86,6 +89,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 32:
|
||||
upgradeFrom31to32(db);
|
||||
break;
|
||||
case 33:
|
||||
upgradeFrom32to33(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
@@ -125,6 +131,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 31:
|
||||
downgradeFrom32to31(db);
|
||||
break;
|
||||
case 32:
|
||||
downgradeFrom33to32(db);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
}
|
||||
@@ -459,6 +468,52 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add timezone to Track.
|
||||
*/
|
||||
private void upgradeFrom32to33(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE tracks ADD COLUMN starttime_offset INTEGER");
|
||||
|
||||
ZoneRules zoneRules = ZoneOffset.systemDefault().getRules();
|
||||
|
||||
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime"}, null, null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow("starttime");
|
||||
do {
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("starttime_offset", zoneRules.getOffset(Instant.ofEpochMilli(startTime)).getTotalSeconds());
|
||||
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.getId())});
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom33to32(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("DROP INDEX tracks_uuid_index");
|
||||
|
||||
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
|
||||
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, icon TEXT, uuid BLOB, elevationloss FLOAT)");
|
||||
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, icon, uuid, elevationloss FROM tracks_old");
|
||||
db.execSQL("DROP TABLE tracks_old");
|
||||
|
||||
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
|
||||
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.annotation.NonNull;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -158,7 +159,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
|
||||
writeTrackPoint(trackPoint, sensorPoints);
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
|
||||
sensorPoints.clear();
|
||||
break;
|
||||
case SENSORPOINT:
|
||||
@@ -171,7 +172,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
wroteSegment = true;
|
||||
}
|
||||
|
||||
writeTrackPoint(trackPoint, sensorPoints);
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
|
||||
sensorPoints.clear();
|
||||
break;
|
||||
default:
|
||||
@@ -230,6 +231,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd"
|
||||
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
|
||||
+ " http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
|
||||
+ " http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
|
||||
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
|
||||
}
|
||||
}
|
||||
@@ -248,7 +250,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
writeMarker(marker);
|
||||
writeMarker(track.getZoneOffset(), marker);
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
@@ -256,13 +258,13 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeMarker(Marker marker) {
|
||||
public void writeMarker(ZoneOffset zoneOffset, Marker marker) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
|
||||
if (marker.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude().toM()) + "</ele>");
|
||||
}
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime(), zoneOffset) + "</time>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
|
||||
printWriter.println("<desc>" + StringUtils.formatCData(marker.getDescription()) + "</desc>");
|
||||
printWriter.println("<type>" + StringUtils.formatCData(marker.getCategory()) + "</type>");
|
||||
@@ -310,7 +312,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
printWriter.println("</trkseg>");
|
||||
}
|
||||
|
||||
public void writeTrackPoint(TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
|
||||
public void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
|
||||
if (printWriter != null) {
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
|
||||
@@ -319,7 +321,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
|
||||
}
|
||||
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
|
||||
|
||||
{
|
||||
String trackPointExtensionContent = "";
|
||||
|
||||
@@ -27,8 +27,8 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -110,10 +110,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
if (hasMultipleTracks) {
|
||||
writeMultiTrackBegin();
|
||||
}
|
||||
//TODO Why use startTime of first track for the others?
|
||||
Instant startTime = tracks[0].getTrackStatistics().getStartTime();
|
||||
for (Track track : tracks) {
|
||||
Duration offset = Duration.between(track.getTrackStatistics().getStartTime(), startTime);
|
||||
writeLocations(track);
|
||||
}
|
||||
if (hasMultipleTracks) {
|
||||
@@ -142,7 +139,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
hasMarkers = true;
|
||||
}
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
writeMarker(marker);
|
||||
writeMarker(marker, track.getZoneOffset());
|
||||
|
||||
cursor.moveToNext();
|
||||
}
|
||||
@@ -172,12 +169,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
case SEGMENT_START_AUTOMATIC:
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
writeTrackPoint(trackPoint);
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint);
|
||||
wroteSegment = true;
|
||||
break;
|
||||
case SEGMENT_END_MANUAL:
|
||||
if (!wroteSegment) writeOpenSegment();
|
||||
writeTrackPoint(trackPoint);
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint);
|
||||
writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
break;
|
||||
@@ -188,7 +185,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
}
|
||||
writeTrackPoint(trackPoint);
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||
@@ -268,14 +265,14 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeMarker(Marker marker) {
|
||||
private void writeMarker(Marker marker, ZoneOffset zoneOffset) {
|
||||
if (printWriter != null) {
|
||||
boolean existsPhoto = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), marker.getPhotoURI()) != null;
|
||||
if (marker.hasPhoto() && exportPhotos && existsPhoto) {
|
||||
float heading = getHeading(marker.getTrackId(), marker.getLocation());
|
||||
writePhotoOverlay(marker, heading);
|
||||
writePhotoOverlay(marker, heading, zoneOffset);
|
||||
} else {
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation());
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), zoneOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,12 +369,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void writeTrackPoint(TrackPoint trackPoint) {
|
||||
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
printWriter.println("<gx:coord>" + (trackPoint.hasLocation() ? getCoordinates(trackPoint.getLocation(), " ") : "") + "</gx:coord>");
|
||||
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
|
||||
} else {
|
||||
printWriter.println("<gx:coord/>");
|
||||
}
|
||||
@@ -420,12 +417,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
* @param description the description
|
||||
* @param location the location
|
||||
*/
|
||||
private void writePlacemark(String name, String category, String description, Location location) {
|
||||
private void writePlacemark(String name, String category, String description, Location location, ZoneOffset zoneOffset) {
|
||||
if (location != null) {
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
|
||||
writeCategory(category);
|
||||
printWriter.println("<Point>");
|
||||
@@ -435,7 +432,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private void writePhotoOverlay(Marker marker, float heading) {
|
||||
private void writePhotoOverlay(Marker marker, float heading, ZoneOffset zoneOffset) {
|
||||
printWriter.println("<PhotoOverlay>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
|
||||
printWriter.println("<description>" + StringUtils.formatCData(marker.getDescription()) + "</description>");
|
||||
@@ -446,7 +443,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.print("<heading>" + heading + "</heading>");
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(marker.getLocation()) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
|
||||
writeCategory(marker.getCategory());
|
||||
|
||||
@@ -472,8 +469,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private String getTime(Location location) {
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()));
|
||||
private String getTime(ZoneOffset zoneOffset, Location location) {
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()), zoneOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,8 @@ import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -86,6 +87,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
|
||||
private final Context context;
|
||||
|
||||
private ZoneOffset zoneOffset;
|
||||
|
||||
// Belongs to the current track
|
||||
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
@@ -157,7 +160,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
onMarkerEnd();
|
||||
break;
|
||||
case TAG_TRACK:
|
||||
trackImporter.setTrack(context, name, uuid, description, category, null);
|
||||
trackImporter.setTrack(context, name, uuid, description, category, null, zoneOffset);
|
||||
zoneOffset = null;
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
onTrackSegmentEnd();
|
||||
@@ -256,14 +260,17 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
|
||||
|
||||
private TrackPoint createTrackPoint() throws ParsingException {
|
||||
Instant parsedTime;
|
||||
OffsetDateTime parsedTime;
|
||||
try {
|
||||
parsedTime = StringUtils.parseTime(time);
|
||||
if (zoneOffset == null) {
|
||||
zoneOffset = parsedTime.getOffset();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime);
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime.toInstant());
|
||||
if (latitude == null || longitude == null) {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -74,6 +76,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private final Context context;
|
||||
|
||||
// Belongs to the current track
|
||||
private ZoneOffset zoneOffset;
|
||||
|
||||
private final ArrayList<Instant> whenList = new ArrayList<>();
|
||||
private final ArrayList<Location> locationList = new ArrayList<>();
|
||||
|
||||
@@ -157,7 +161,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
onMarkerLocationEnd();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
trackImporter.setTrack(context, name, uuid, description, category, icon);
|
||||
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
|
||||
zoneOffset = null;
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
onTrackSegmentEnd();
|
||||
@@ -195,7 +200,15 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
break;
|
||||
case TAG_WHEN:
|
||||
if (content != null) {
|
||||
whenList.add(StringUtils.parseTime(content.trim()));
|
||||
try {
|
||||
OffsetDateTime time = StringUtils.parseTime(content.trim());
|
||||
if (zoneOffset == null) {
|
||||
zoneOffset = time.getOffset();
|
||||
}
|
||||
whenList.add(time.toInstant());
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", content.trim())), e);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -4,6 +4,10 @@ import androidx.annotation.NonNull;
|
||||
|
||||
public class ParsingException extends RuntimeException {
|
||||
|
||||
protected ParsingException(@NonNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
protected ParsingException(@NonNull String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@@ -89,8 +91,8 @@ public class TrackImporter {
|
||||
this.markers.addAll(markers);
|
||||
}
|
||||
|
||||
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
|
||||
track = new Track();
|
||||
void setTrack(Context context, String name, String uuid, String description, String category, String icon, @Nullable ZoneOffset zoneOffset) {
|
||||
track = new Track(zoneOffset != null ? zoneOffset : ZoneOffset.UTC);
|
||||
track.setName(name != null ? name : "");
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
@@ -47,9 +49,9 @@ class TrackRecordingManager {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
Track.Id start(TrackPoint segmentStartTrackPoint) {
|
||||
Track.Id start(TrackPoint segmentStartTrackPoint, ZoneOffset zoneOffset) {
|
||||
// Create new track
|
||||
Track track = new Track();
|
||||
Track track = new Track(zoneOffset);
|
||||
trackId = contentProviderUtils.insertTrack(track);
|
||||
track.setId(trackId);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
@@ -222,7 +223,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
}
|
||||
|
||||
// Set recording status
|
||||
Track.Id trackId = trackRecordingManager.start(trackPointCreator.createSegmentStartManual());
|
||||
TrackPoint segmentStartManual = trackPointCreator.createSegmentStartManual();
|
||||
ZoneOffset zoneOffset = ZoneOffset.systemDefault().getRules().getOffset(segmentStartManual.getTime());
|
||||
Track.Id trackId = trackRecordingManager.start(segmentStartManual, zoneOffset);
|
||||
updateRecordingStatus(RecordingStatus.record(trackId));
|
||||
|
||||
startRecording();
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -49,8 +50,6 @@ public class StringUtils {
|
||||
|
||||
private static final String TAG = StringUtils.class.getSimpleName();
|
||||
|
||||
private static final String COORDINATE_DEGREE = "\u00B0";
|
||||
|
||||
private StringUtils() {
|
||||
}
|
||||
|
||||
@@ -70,10 +69,12 @@ public class StringUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the time using the ISO 8601 date time format with fractional seconds in UTC time zone.
|
||||
* Formats the time using the ISO 8601 date time format with fractional seconds.
|
||||
*/
|
||||
public static String formatDateTimeIso8601(@NonNull Instant time) {
|
||||
return time.toString();
|
||||
public static String formatDateTimeIso8601(@NonNull Instant time, ZoneOffset zoneOffset) {
|
||||
return time
|
||||
.atOffset(zoneOffset)
|
||||
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,14 +287,14 @@ public class StringUtils {
|
||||
*
|
||||
* @param xmlDateTime the XML date time string
|
||||
*/
|
||||
public static Instant parseTime(String xmlDateTime) {
|
||||
public static OffsetDateTime parseTime(String xmlDateTime) {
|
||||
try {
|
||||
TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parseBest(xmlDateTime, ZonedDateTime::from, LocalDateTime::from);
|
||||
if (t instanceof LocalDateTime) {
|
||||
Log.w(TAG, "Date does not contain timezone information: using UTC.");
|
||||
t = ((LocalDateTime) t).atZone(ZoneOffset.UTC);
|
||||
}
|
||||
return Instant.from(t);
|
||||
return OffsetDateTime.from(t);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Invalid XML dateTime value");
|
||||
throw e;
|
||||
|
||||
Reference in New Issue
Block a user