Skip to content

Commit

Permalink
Timezone support: store timezone offset per Track.
Browse files Browse the repository at this point in the history
When a recording is started the offset of the phone is used.
Timezone is exported to KML and GPX.

Fixes #301.
  • Loading branch information
dennisguse committed Dec 28, 2021
1 parent 7d0d87e commit d0975e0
Show file tree
Hide file tree
Showing 16 changed files with 175 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,12 +12,11 @@

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 {

Expand All @@ -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" +
Expand All @@ -41,7 +44,8 @@ public void writeCloseSegment_only_write_sensordata_if_present() {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOExc
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());

Expand Down Expand Up @@ -338,6 +339,7 @@ public void gpx() throws TimeoutException, IOException {
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,9 +34,6 @@
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}.
*
Expand Down Expand Up @@ -141,7 +141,7 @@ private void assertGetTime(String xmlDateTime, int year, int month, int day, int

// 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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/de/dennisguse/opentracks/content/data/Track.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@

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;

Expand All @@ -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.
*/
Expand Down Expand Up @@ -99,6 +113,14 @@ public void setIcon(String icon) {
this.icon = icon;
}

public ZoneOffset getZoneOffset() {
return zoneOffset;
}

public OffsetDateTime getStartTime() {
return trackStatistics.getStartTime().atOffset(zoneOffset);
}

@NonNull
public TrackStatistics getTrackStatistics() {
return trackStatistics;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 + ")";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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;
Expand Down Expand Up @@ -98,6 +99,7 @@ public static Track createTrack(Cursor cursor) {
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);
Expand All @@ -109,7 +111,7 @@ public static Track createTrack(Cursor cursor) {
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)));
Expand All @@ -126,6 +128,7 @@ public static Track createTrack(Cursor cursor) {
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
}

if (!cursor.isNull(startTimeIndex)) {
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
}
Expand Down Expand Up @@ -288,6 +291,7 @@ private ContentValues createContentValues(Track track) {
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

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;
Expand All @@ -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());
Expand Down Expand Up @@ -86,6 +89,9 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
case 32:
upgradeFrom31to32(db);
break;
case 33:
upgradeFrom32to33(db);
break;

default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
Expand Down Expand Up @@ -125,6 +131,9 @@ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
case 31:
downgradeFrom32to31(db);
break;
case 32:
downgradeFrom33to32(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
Expand Down Expand Up @@ -459,6 +468,52 @@ private void downgradeFrom32to31(SQLiteDatabase db) {

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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand Down Expand Up @@ -158,7 +159,7 @@ private void writeTrackPoints(Track track) throws InterruptedException {
writeOpenSegment();
wroteSegment = true;

writeTrackPoint(trackPoint, sensorPoints);
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
sensorPoints.clear();
break;
case SENSORPOINT:
Expand All @@ -171,7 +172,7 @@ private void writeTrackPoints(Track track) throws InterruptedException {
wroteSegment = true;
}

writeTrackPoint(trackPoint, sensorPoints);
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
sensorPoints.clear();
break;
default:
Expand Down Expand Up @@ -230,6 +231,7 @@ public void writeHeader() {
+ " 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\">");
}
}
Expand All @@ -248,21 +250,21 @@ private void writeMarkers(Track track) throws InterruptedException {
throw new InterruptedException();
}
Marker marker = contentProviderUtils.createMarker(cursor);
writeMarker(marker);
writeMarker(track.getZoneOffset(), marker);

cursor.moveToNext();
}
}
}
}

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>");
Expand Down Expand Up @@ -310,7 +312,7 @@ public void writeCloseSegment() {
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()) + ">");
Expand All @@ -319,7 +321,7 @@ public void writeTrackPoint(TrackPoint trackPoint, List<TrackPoint> sensorPoints
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 = "";
Expand Down
Loading

0 comments on commit d0975e0

Please sign in to comment.