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:
Dennis Guse
2021-12-07 22:39:40 +01:00
parent 7193171a09
commit 6d04659370
16 changed files with 175 additions and 55 deletions
@@ -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 {