Update KML writer to output a track using gx:MultiTrack and gx:Track.

This commit is contained in:
Jimmy Shih
2012-02-22 17:18:38 -08:00
parent 8fc43ff3fb
commit ccb4190314
9 changed files with 453 additions and 247 deletions
+16
View File
@@ -559,6 +559,22 @@ limitations under the License.
%1$s time. E.g, '2/15/2012 1:40 PM'"> %1$s time. E.g, '2/15/2012 1:40 PM'">
Recorded: %1$s Recorded: %1$s
</string> </string>
<string name="description_sensor_battery_level"
translation_description="Label for battery level with its unit percentage (%).">
Battery level (%)
</string>
<string name="description_sensor_cadence"
translation_description="Label for cadence with its unit revolutions per minute (rpm).">
Cadence (rpm)
</string>
<string name="description_sensor_heart_rate"
translation_description="Label for heart rate with its unit beats per minute (bpm).">
Heart rate (bpm)
</string>
<string name="description_sensor_power"
translation_description="Label for power with its unit watt (W).">
Power (W)
</string>
<string name="description_total_distance" <string name="description_total_distance"
translation_description="Total distance description. translation_description="Total distance description.
@@ -102,6 +102,16 @@ public class CsvTrackWriter implements TrackFormatWriter {
Integer.toString(segmentIdx)); Integer.toString(segmentIdx));
} }
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override @Override
public void writeWaypoint(Waypoint waypoint) { public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation(); Location location = waypoint.getLocation();
@@ -150,6 +150,16 @@ public class GpxTrackWriter implements TrackFormatWriter {
} }
} }
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override @Override
public void writeWaypoint(Waypoint waypoint) { public void writeWaypoint(Waypoint waypoint) {
if (pw != null) { if (pw != null) {
@@ -17,220 +17,344 @@ package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl; import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
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.io.file.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import android.content.Context; import android.content.Context;
import android.location.Location; import android.location.Location;
import android.os.Build;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.Vector; import java.util.ArrayList;
/** /**
* Write track as KML to a file. * Writer to write a track as KML.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class KmlTrackWriter implements TrackFormatWriter { public class KmlTrackWriter implements TrackFormatWriter {
private final Vector<Double> distances = new Vector<Double>(); private static final String WAYPOINT_STYLE = "waypoint";
private final Vector<Double> elevations = new Vector<Double>(); private static final String STATISTICS_STYLE = "statistics";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String BATTER_LEVEL = "battery_level";
private final Context context;
private final DescriptionGenerator descriptionGenerator; private final DescriptionGenerator descriptionGenerator;
private PrintWriter pw = null;
private Track track; private Track track;
private PrintWriter printWriter;
private ArrayList<Integer> powerList = new ArrayList<Integer>();
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
private ArrayList<Integer> heartRateList = new ArrayList<Integer>();
private ArrayList<Integer> batteryLevelList = new ArrayList<Integer>();
private boolean hasPower;
private boolean hasCadence;
private boolean hasHeartRate;
private boolean hasBatteryLevel;
public KmlTrackWriter(Context context) { public KmlTrackWriter(Context context) {
descriptionGenerator = new DescriptionGeneratorImpl(context); this(context, new DescriptionGeneratorImpl(context));
} }
@VisibleForTesting @VisibleForTesting
KmlTrackWriter(DescriptionGenerator descriptionGenerator) { KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.descriptionGenerator = descriptionGenerator; this.descriptionGenerator = descriptionGenerator;
} }
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override @Override
public String getExtension() { public String getExtension() {
return TrackFileFormat.KML.getExtension(); return TrackFileFormat.KML.getExtension();
} }
@Override @Override
public void writeHeader() { public void prepare(Track aTrack, OutputStream outputStream) {
if (pw != null) { this.track = aTrack;
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); this.printWriter = new PrintWriter(outputStream);
pw.print("<kml"); }
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("<Document>");
pw.format("<atom:author><atom:name>My Tracks running on %s"
+ "</atom:name></atom:author>\n", Build.MODEL);
pw.println("<name>" + StringUtils.stringAsCData(track.getName()) @Override
+ "</name>"); public void close() {
pw.println("<description>" if (printWriter != null) {
+ StringUtils.stringAsCData(track.getDescription()) printWriter.close();
+ "</description>"); printWriter = null;
writeStyles(); }
}
@Override
public void writeHeader() {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\">");
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
printWriter.println(
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>" + StringUtils.stringAsCData(
context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(
START_STYLE, "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png", 32, 1);
writePlacemarkerStyle(
END_STYLE, "http://maps.google.com/mapfiles/kml/paddle/red-circle.png", 32, 1);
writePlacemarkerStyle(
STATISTICS_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", 20, 2);
writePlacemarkerStyle(
WAYPOINT_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png", 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
writeSensorStyle(BATTER_LEVEL, context.getString(R.string.description_sensor_battery_level));
printWriter.println("</Schema>");
} }
} }
@Override @Override
public void writeFooter() { public void writeFooter() {
if (pw != null) { if (printWriter != null) {
pw.println("</Document>"); printWriter.println("</Document>");
pw.println("</kml>"); printWriter.println("</kml>");
} }
} }
@Override @Override
public void writeBeginTrack(Location firstPoint) { public void writeBeginWaypoints() {
if (pw != null) { if (printWriter != null) {
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle", printWriter.println(
firstPoint); "<Folder><name>" + StringUtils.stringAsCData(context.getString(R.string.menu_markers))
pw.println("<Placemark>"); + "</name>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
pw.println("<styleUrl>#track</styleUrl>");
pw.println("<MultiGeometry>");
} }
} }
@Override @Override
public void writeEndTrack(Location lastPoint) { public void writeEndWaypoints() {
if (pw != null) { if (printWriter != null) {
pw.println("</MultiGeometry>"); printWriter.println("</Folder>");
pw.println("</Placemark>"); }
String description = descriptionGenerator.generateTrackDescription( }
track, distances, elevations);
writePlacemark("(End)", description, "#sh_red-circle", lastPoint); @Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
String styleName = waypoint.getType() == Waypoint.TYPE_STATISTICS ? STATISTICS_STYLE
: WAYPOINT_STYLE;
writePlacemark(
waypoint.getName(), waypoint.getDescription(), styleName, waypoint.getLocation());
}
}
@Override
public void writeBeginTrack(Location firstLocation) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, track.getDescription(), START_STYLE, firstLocation);
printWriter.println("<Placemark>");
printWriter.println(
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
@Override
public void writeEndTrack(Location lastLocation) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, null, null);
writePlacemark(name, description, END_STYLE, lastLocation);
} }
} }
@Override @Override
public void writeOpenSegment() { public void writeOpenSegment() {
if (pw != null) { if (printWriter != null) {
pw.print("<LineString><coordinates>"); printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
hasBatteryLevel = false;
powerList.clear();
cadenceList.clear();
heartRateList.clear();
batteryLevelList.clear();
} }
} }
@Override @Override
public void writeCloseSegment() { public void writeCloseSegment() {
if (pw != null) { if (printWriter != null) {
pw.println("</coordinates></LineString>"); printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
if (hasBatteryLevel) {
writeSensorData(batteryLevelList, BATTER_LEVEL);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
} }
} }
@Override @Override
public void writeLocation(Location l) { public void writeLocation(Location location) {
if (pw != null) { if (printWriter != null) {
pw.print(l.getLongitude() + "," + l.getLatitude() + "," printWriter.println("<when>" + StringUtils.formatXmlDateTime(location.getTime()) + "</when>");
+ l.getAltitude() + " "); printWriter.println(
"<gx:coord>" + location.getLongitude() + " " + location.getLatitude() + " "
+ location.getAltitude() + "</gx:coord>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
int power = -1;
int cadence = -1;
int heartRate = -1;
int batteryLevel = -1;
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasPower = true;
power = sensorData.getValue();
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasCadence = true;
cadence = sensorData.getValue();
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasHeartRate = true;
heartRate = sensorData.getValue();
}
}
if (sensorDataSet.hasBatteryLevel()) {
SensorData sensorData = sensorDataSet.getBatteryLevel();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasBatteryLevel = true;
batteryLevel = sensorData.getValue();
}
}
}
powerList.add(power);
cadenceList.add(cadence);
heartRateList.add(heartRate);
batteryLevelList.add(batteryLevel);
}
} }
} }
private String getPinStyle(Waypoint waypoint) { /**
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) { * Writes the sensor data.
return "#sh_ylw-pushpin"; *
* @param list a list of sensor data
* @param name the name of the sensor data
*/
private void writeSensorData(ArrayList<Integer> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
} }
// Try to find the icon color. printWriter.println("</gx:SimpleArrayData>");
// The string should be of the form:
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
int slash = waypoint.getIcon().lastIndexOf('/');
int png = waypoint.getIcon().lastIndexOf('.');
if ((slash != -1) && (slash < png)) {
String color = waypoint.getIcon().substring(slash + 1, png);
return "#sh_" + color + "-pushpin";
}
return "#sh_blue-pushpin";
} }
@Override /**
public void writeWaypoint(Waypoint waypoint) { * Writes a placemark.
if (pw != null) { *
writePlacemark( * @param name the name of the placemark
waypoint.getName(), * @param description the description
waypoint.getDescription(), * @param styleName the style name
getPinStyle(waypoint), * @param location the location
waypoint.getLocation()); */
} private void writePlacemark(
} String name, String description, String styleName, Location location) {
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
private void writeStyles() {
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
+ "<width>4</width></LineStyle></Style>");
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "grn-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "red-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "ylw-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "blue-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "grn-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "red-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
}
private void writePlacemark(String name, String description, String style,
Location location) {
if (location != null) { if (location != null) {
pw.println("<Placemark>"); printWriter.println("<Placemark>");
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>"); printWriter.println(
pw.println(" <description>" + StringUtils.stringAsCData(description) "<description>" + StringUtils.stringAsCData(description) + "</description>");
+ "</description>"); printWriter.println("<name>" + StringUtils.stringAsCData(name) + "</name>");
pw.println(" <styleUrl>" + style + "</styleUrl>"); printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
pw.println(" <Point>"); printWriter.println("<Point>");
pw.println(" <coordinates>" + location.getLongitude() + "," printWriter.println(
+ location.getLatitude() + "</coordinates>"); "<coordinates>" + location.getLongitude() + "," + location.getLatitude() + ","
pw.println(" </Point>"); + location.getAltitude() + "</coordinates>");
pw.println("</Placemark>"); printWriter.println("</Point>");
printWriter.println("</Placemark>");
} }
} }
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\"><LineStyle>");
printWriter.println("<color>7f0000ff</color><width>4</width>");
printWriter.println("</LineStyle></Style>");
}
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param displayName the sensor display name
*/
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println(
"<displayName>" + StringUtils.stringAsCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
} }
@@ -291,6 +291,16 @@ public class TcxTrackWriter implements TrackFormatWriter {
pw.println("</TrainingCenterDatabase>"); pw.println("</TrainingCenterDatabase>");
} }
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override @Override
public void writeWaypoint(Waypoint waypoint) { public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere. // TODO Write out the waypoints somewhere.
@@ -23,12 +23,15 @@ import android.location.Location;
import java.io.OutputStream; import java.io.OutputStream;
/** /**
* Interface for writing data to a specific track file format. * Interface for writing a track data to a specific file format.
* *
* The expected sequence of calls is: * The expected sequence of calls is:
* <ol> * <ol>
* <li>{@link #prepare} * <li>{@link #prepare}
* <li>{@link #writeHeader} * <li>{@link #writeHeader}
* <li>{@link #writeBeginWaypoints}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeEndWaypoints}
* <li>{@link #writeBeginTrack} * <li>{@link #writeBeginTrack}
* <li>For each segment: * <li>For each segment:
* <ol> * <ol>
@@ -37,8 +40,8 @@ import java.io.OutputStream;
* <li>{@link #writeCloseSegment} * <li>{@link #writeCloseSegment}
* </ol> * </ol>
* <li>{@link #writeEndTrack} * <li>{@link #writeEndTrack}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeFooter} * <li>{@link #writeFooter}
* <li>{@link #close}
* </ol> * </ol>
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
@@ -46,68 +49,78 @@ import java.io.OutputStream;
public interface TrackFormatWriter { public interface TrackFormatWriter {
/** /**
* Sets up the writer to write the given track to the given output. * Gets the file extension (i.e. gpx, kml, ...)
*
* @param track the track to write
* @param out the stream to write the track contents to
*/ */
void prepare(Track track, OutputStream out); public String getExtension();
/** /**
* @return The file extension (i.e. gpx, kml, ...) * Sets up the writer to write the given track.
*
* @param track the track to write
* @param outputStream the output stream to write the track to
*/ */
String getExtension(); public void prepare(Track track, OutputStream outputStream);
/**
* Closes the underlying file handler.
*/
public void close();
/** /**
* Writes the header. * Writes the header.
* This is chance for classes to write out opening information.
*/ */
void writeHeader(); public void writeHeader();
/** /**
* Writes the footer. * Writes the footer.
* This is chance for classes to write out closing information.
*/ */
void writeFooter(); public void writeFooter();
/** /**
* Write the given location object. * Writes the beginning of the waypoints.
*/
public void writeBeginWaypoints();
/**
* Writes the end of the waypoints.
*/
public void writeEndWaypoints();
/**
* Writes a waypoint.
* *
* TODO Add some flexible handling of other sensor data. * @param waypoint the waypoint
*/
public void writeWaypoint(Waypoint waypoint);
/**
* Writes the beginning of the track.
* *
* @param location the location to write * @param firstLocation the first location
*/ */
void writeLocation(Location location) throws InterruptedException; public void writeBeginTrack(Location firstLocation);
/** /**
* Write a way point. * Writes the end of the track.
* *
* @param waypoint * @param lastLocation the last location
*/ */
void writeWaypoint(Waypoint waypoint); public void writeEndTrack(Location lastLocation);
/** /**
* Write the beginning of a track. * Writes the statements necessary to open a new segment.
*/ */
void writeBeginTrack(Location firstPoint); public void writeOpenSegment();
/** /**
* Write the end of a track. * Writes the statements necessary to close a segment.
*/ */
void writeEndTrack(Location lastPoint); public void writeCloseSegment();
/** /**
* Write the statements necessary to open a new segment. * Writes a location.
*
* @param location the location
*/ */
void writeOpenSegment(); public void writeLocation(Location location);
/**
* Write the statements necessary to close a segment.
*/
void writeCloseSegment();
/**
* Close the underlying file handle.
*/
void close();
} }
@@ -269,12 +269,17 @@ class TrackWriterImpl implements TrackWriter {
Cursor cursor = null; Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0, cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS); Constants.MAX_LOADED_WAYPOINTS_POINTS);
boolean hasWaypoints = false;
if (cursor != null) { if (cursor != null) {
try { try {
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional // Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment. // as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (!hasWaypoints) {
writer.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint wpt = providerUtils.createWaypoint(cursor); Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt); writer.writeWaypoint(wpt);
} }
@@ -283,6 +288,9 @@ class TrackWriterImpl implements TrackWriter {
cursor.close(); cursor.close();
} }
} }
if (hasWaypoints) {
writer.writeEndWaypoints();
}
} }
/** /**
@@ -143,6 +143,16 @@ public class StringUtils {
static { static {
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC")); BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
} }
/**
* Formats the time to the XML date time format.
*
* @param time time in milliseconds
*/
public static final String formatXmlDateTime(long time) {
return BASE_XML_DATE_FORMAT.format(new Date(time));
}
private static final Pattern XML_DATE_EXTRAS_PATTERN = private static final Pattern XML_DATE_EXTRAS_PATTERN =
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$"); Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
@@ -1,4 +1,19 @@
// Copyright 2010 Google Inc. All Rights Reserved. /*
* 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.io.file; package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.DescriptionGenerator;
@@ -14,7 +29,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
/** /**
* Tests for the KML track exporter. * Tests for {@link KmlTrackWriter}.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -25,13 +40,10 @@ public class KmlTrackWriterTest extends TrackFormatWriterTest {
* A fake version of {@link DescriptionGenerator} which returns a fixed track * A fake version of {@link DescriptionGenerator} which returns a fixed track
* description, thus not depending on the context. * description, thus not depending on the context.
*/ */
private class MockDescriptionGenerator implements DescriptionGenerator { private class FakeDescriptionGenerator implements DescriptionGenerator {
@Override @Override
public String generateTrackDescription( public String generateTrackDescription(
Track trackToDescribe, Vector<Double> distances, Vector<Double> elevations) { Track aTrack, Vector<Double> distances, Vector<Double> elevations) {
assertSame(KmlTrackWriterTest.super.track, trackToDescribe);
assertTrue(distances.isEmpty());
assertTrue(elevations.isEmpty());
return FULL_TRACK_DESCRIPTION; return FULL_TRACK_DESCRIPTION;
} }
@@ -42,7 +54,7 @@ public class KmlTrackWriterTest extends TrackFormatWriterTest {
} }
public void testXmlOutput() throws Exception { public void testXmlOutput() throws Exception {
KmlTrackWriter writer = new KmlTrackWriter(new MockDescriptionGenerator()); KmlTrackWriter writer = new KmlTrackWriter(getContext(), new FakeDescriptionGenerator());
String result = writeTrack(writer); String result = writeTrack(writer);
Document doc = parseXmlDocument(result); Document doc = parseXmlDocument(result);
@@ -51,65 +63,58 @@ public class KmlTrackWriterTest extends TrackFormatWriterTest {
assertEquals(TRACK_NAME, getChildTextValue(docTag, "name")); assertEquals(TRACK_NAME, getChildTextValue(docTag, "name"));
assertEquals(TRACK_DESCRIPTION, getChildTextValue(docTag, "description")); assertEquals(TRACK_DESCRIPTION, getChildTextValue(docTag, "description"));
// There are 5 placemarks - start, segments, end, waypoint 1, waypoint 2 // There are 5 placemarks - start, segments, end, waypoint1, waypoint2
List<Element> placemarkTags = getChildElements(docTag, "Placemark", 5); List<Element> placemarkTags = getChildElements(docTag, "Placemark", 5);
assertTagIsPlacemark(placemarkTags.get(0), assertTagIsPlacemark(
"(Start)", TRACK_DESCRIPTION, location1); placemarkTags.get(0), TRACK_NAME + " (Start)", TRACK_DESCRIPTION, location1);
assertTagIsPlacemark(placemarkTags.get(2), assertTagIsPlacemark(
"(End)", FULL_TRACK_DESCRIPTION, location4); placemarkTags.get(2), TRACK_NAME + " (End)", FULL_TRACK_DESCRIPTION, location4);
assertTagIsPlacemark(placemarkTags.get(3), assertTagIsPlacemark(placemarkTags.get(3), WAYPOINT1_NAME, WAYPOINT1_DESCRIPTION, location2);
WAYPOINT1_NAME, WAYPOINT1_DESCRIPTION, location2); assertTagIsPlacemark(placemarkTags.get(4), WAYPOINT2_NAME, WAYPOINT2_DESCRIPTION, location3);
assertTagIsPlacemark(placemarkTags.get(4),
WAYPOINT2_NAME, WAYPOINT2_DESCRIPTION, location3);
Element trackPlacemarkTag = placemarkTags.get(1); Element trackPlacemarkTag = placemarkTags.get(1);
assertEquals(TRACK_NAME, getChildTextValue(trackPlacemarkTag, "name")); assertEquals(TRACK_NAME, getChildTextValue(trackPlacemarkTag, "name"));
assertEquals(TRACK_DESCRIPTION, assertEquals(TRACK_DESCRIPTION, getChildTextValue(trackPlacemarkTag, "description"));
getChildTextValue(trackPlacemarkTag, "description")); Element multiTrackTag = getChildElement(trackPlacemarkTag, "gx:MultiTrack");
Element geometryTag = getChildElement(trackPlacemarkTag, "MultiGeometry"); List<Element> trackTags = getChildElements(multiTrackTag, "gx:Track", 2);
List<Element> segmentTags = getChildElements(geometryTag, "LineString", 2); assertTagHasPoints(trackTags.get(0), location1, location2);
assertTagHasPoints(segmentTags.get(0), location1, location2); assertTagHasPoints(trackTags.get(1), location3, location4);
assertTagHasPoints(segmentTags.get(1), location3, location4);
} }
/** /**
* Asserts that the given XML tag is a placemark with the given properties. * Asserts that the given tag is a placemark with the given properties.
* *
* @param tag the tag to analyze * @param tag the tag
* @param name the expected name for the placemark * @param name the expected placemark name
* @param description the expected description for the placemark * @param description the expected placemark description
* @param location the expected location of the placemark * @param location the expected placemark location
*/ */
private void assertTagIsPlacemark(Element tag, String name, private void assertTagIsPlacemark(
String description, Location location) { Element tag, String name, String description, Location location) {
assertEquals(name, getChildTextValue(tag, "name")); assertEquals(name, getChildTextValue(tag, "name"));
assertEquals(description, getChildTextValue(tag, "description")); assertEquals(description, getChildTextValue(tag, "description"));
Element pointTag = getChildElement(tag, "Point"); Element pointTag = getChildElement(tag, "Point");
String expectedCoords = String expected = location.getLongitude() + "," + location.getLatitude() + ","
location.getLongitude() + "," + location.getLatitude(); + location.getAltitude();
String actualCoords = getChildTextValue(pointTag, "coordinates"); String actual = getChildTextValue(pointTag, "coordinates");
assertEquals(expectedCoords, actualCoords); assertEquals(expected, actual);
} }
/** /**
* Asserts that the given tag has a "coordinates" subtag with the given * Asserts that the given tag has a list of "gx:coord" subtags matching the
* locations. * expected locations.
* *
* @param tag the tag to analyze * @param tag the parent tag
* @param locs the locations to expect in the coordinates * @param locations list of expected locations
*/ */
private void assertTagHasPoints(Element tag, Location... locs) { private void assertTagHasPoints(Element tag, Location... locations) {
StringBuilder expectedBuilder = new StringBuilder(); List<Element> coordTags = getChildElements(tag, "gx:coord", locations.length);
for (Location loc : locs) { for (int i = 0; i < locations.length; i++) {
expectedBuilder.append(loc.getLongitude()); Location location = locations[i];
expectedBuilder.append(','); String expected = location.getLongitude() + " " + location.getLatitude() + " "
expectedBuilder.append(loc.getLatitude()); + location.getAltitude();
expectedBuilder.append(','); String actual = coordTags.get(i).getFirstChild().getTextContent();
expectedBuilder.append(loc.getAltitude()); assertEquals(expected, actual);
expectedBuilder.append(' ');
} }
String expectedCoordinates = expectedBuilder.toString().trim();
String actualCoordinates = getChildTextValue(tag, "coordinates").trim();
assertEquals(expectedCoordinates, actualCoordinates);
} }
} }