Export track in kmz or kml

This commit is contained in:
Jimmy Shih
2013-09-03 12:56:44 -07:00
parent b8bd8d62ce
commit 71f03996f5
9 changed files with 297 additions and 91 deletions
@@ -0,0 +1,83 @@
/*
* Copyright 2013 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;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
/**
* Abstract class for {@link TrackExporter}.
*
* @author Jimmy Shih
*/
abstract class AbstractTrackExporter implements TrackExporter {
private static final String TAG = AbstractTrackExporter.class.getSimpleName();
private Thread writeThread;
private boolean success = false;
@Override
public void writeTrack(final OutputStream outputStream) {
writeThread = new Thread() {
@Override
public void run() {
try {
performWrite(outputStream);
success = true;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to perform write", e);
success = false;
} catch (IOException e) {
Log.e(TAG, "Unable to perform write", e);
success = false;
}
}
};
writeThread.start();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for write thread to finish", e);
success = false;
}
}
@Override
public void stopWriteTrack() {
if (writeThread != null && writeThread.isAlive()) {
Log.i(TAG, "Attempting to stop write thread");
writeThread.interrupt();
try {
writeThread.join();
Log.i(TAG, "Write thread stopped");
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for write thread to stop", e);
success = false;
}
}
}
@Override
public boolean isSuccess() {
return success;
}
abstract void performWrite(OutputStream outputStream) throws InterruptedException, IOException;
}
@@ -28,7 +28,6 @@ import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.OutputStream;
@@ -38,18 +37,13 @@ import java.io.OutputStream;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class FileTrackExporter implements TrackExporter {
private final static String TAG = FileTrackExporter.class.getSimpleName();
public class FileTrackExporter extends AbstractTrackExporter {
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track[] tracks;
private final TrackWriter trackWriter;
private final TrackExporterListener trackExporterListener;
private Thread writeThread;
private boolean success = false;
/**
* Constructor.
*
@@ -59,10 +53,10 @@ public class FileTrackExporter implements TrackExporter {
* @param trackFileFormat the track file format
* @param trackExporterListener the track export listener
*/
public FileTrackExporter(Context context, MyTracksProviderUtils myTracksProviderUtils,
Track[] tracks, TrackFileFormat trackFileFormat,
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
Track[] tracks, TrackFileFormat trackFileFormat, Context context, boolean inZip,
TrackExporterListener trackExporterListener) {
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context),
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context, inZip),
trackExporterListener);
}
@@ -76,55 +70,17 @@ public class FileTrackExporter implements TrackExporter {
}
@Override
public boolean isSuccess() {
return success;
}
@Override
public void writeTrack(final OutputStream outputStream) {
writeThread = new Thread() {
@Override
public void run() {
try {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks[0]);
long startTime = tracks[0].getTripStatistics().getStartTime();
for (int i = 0; i < tracks.length; i++) {
writeWaypoints(tracks[i]);
long offset = tracks[i].getTripStatistics().getStartTime() - startTime;
writeLocations(tracks[i], offset);
}
trackWriter.writeFooter();
trackWriter.close();
success = true;
} catch (InterruptedException e) {
success = false;
}
}
};
writeThread.start();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for write to complete", e);
success = false;
}
}
@Override
public void stopWriteTrack() {
if (writeThread != null && writeThread.isAlive()) {
Log.i(TAG, "Attempting to stop track write");
writeThread.interrupt();
try {
writeThread.join();
Log.i(TAG, "Track write stopped");
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for writer to stop", e);
success = false;
}
void performWrite(OutputStream outputStream) throws InterruptedException {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks[0]);
long startTime = tracks[0].getTripStatistics().getStartTime();
for (int i = 0; i < tracks.length; i++) {
writeWaypoints(tracks[i]);
long offset = tracks[i].getTripStatistics().getStartTime() - startTime;
writeLocations(tracks[i], offset);
}
trackWriter.writeFooter();
trackWriter.close();
}
/**
@@ -35,6 +35,7 @@ import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -68,8 +69,10 @@ public class KmlTrackWriter implements TrackWriter {
TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean inKmz;
private final DescriptionGenerator descriptionGenerator;
private final MyTracksProviderUtils myTracksProviderUtils;
private PrintWriter printWriter;
private ArrayList<Integer> powerList = new ArrayList<Integer>();
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
@@ -78,13 +81,14 @@ public class KmlTrackWriter implements TrackWriter {
private boolean hasCadence;
private boolean hasHeartRate;
public KmlTrackWriter(Context context) {
this(context, new DescriptionGeneratorImpl(context));
public KmlTrackWriter(Context context, boolean inKmz) {
this(context, inKmz, new DescriptionGeneratorImpl(context));
}
@VisibleForTesting
KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
KmlTrackWriter(Context context, boolean inKmz, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.inKmz = inKmz;
this.descriptionGenerator = descriptionGenerator;
this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@@ -335,9 +339,8 @@ public class KmlTrackWriter implements TrackWriter {
if (location != null) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<description>"
+ StringUtils.formatCData(StringUtils.getCategoryDescription(category, description))
+ "</description>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
@@ -345,8 +348,18 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
writeCategory(category);
if (inKmz) {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
+ uri.getLastPathSegment() + "</href></Icon>");
} else {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
printWriter.print("<near>10</near>");
printWriter.print("<leftFov>-60</leftFov>");
@@ -0,0 +1,146 @@
/*
* Copyright 2013 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.database.Cursor;
import android.net.Uri;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* KMZ track exporter.
*
* @author Jimmy Shih
*/
public class KmzTrackExporter extends AbstractTrackExporter {
public static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_EXTENSION = "kmz";
public static final int BUFFER_SIZE = 4096;
public static final String KML_FILE_NAME = "doc.kml";
private final MyTracksProviderUtils myTracksProviderUtils;
private final TrackExporter trackExporter;
private final Track[] tracks;
/**
* Constructor.
*
* @param myTracksProviderUtils the my tracks provider utils
* @param trackExporter the track exporter
* @param tracks the tracks to export
*/
public KmzTrackExporter(
MyTracksProviderUtils myTracksProviderUtils, TrackExporter trackExporter, Track[] tracks) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.trackExporter = trackExporter;
this.tracks = tracks;
}
@Override
public boolean isSuccess() {
return trackExporter.isSuccess() && super.isSuccess();
}
@Override
public void stopWriteTrack() {
trackExporter.stopWriteTrack();
super.stopWriteTrack();
}
@Override
void performWrite(OutputStream outputStream) throws InterruptedException, IOException {
ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = new ZipOutputStream(outputStream);
// Add kml file
ZipEntry zipEntry = new ZipEntry(KML_FILE_NAME);
zipOutputStream.putNextEntry(zipEntry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(byteArrayOutputStream);
zipOutputStream.write(byteArrayOutputStream.toByteArray());
zipOutputStream.closeEntry();
// Add photos
addImages(zipOutputStream);
} finally {
if (zipOutputStream != null) {
zipOutputStream.close();
}
}
}
private void addImages(ZipOutputStream zipOutputStream) throws IOException {
for (Track track : tracks) {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as
* the first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
String photoUrl = waypoint.getPhotoUrl();
if (photoUrl != null && !photoUrl.equals("")) {
addImage(zipOutputStream, photoUrl);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
Uri uri = Uri.parse(photoUrl);
ZipEntry zipEntry = new ZipEntry(
KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment());
zipOutputStream.putNextEntry(zipEntry);
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(new File(uri.getPath()));
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount = 0;
while ((byteCount = fileInputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, byteCount);
}
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
zipOutputStream.closeEntry();
}
}
@@ -99,7 +99,7 @@ public class SaveActivity extends Activity {
saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, directory);
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, directory, playTrack);
saveAsyncTask.execute();
}
}
@@ -34,7 +34,6 @@ import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* Async Task to save tracks to the external storage.
@@ -49,11 +48,12 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final TrackFileFormat trackFileFormat;
private final long[] trackIds;
private final File directory;
private final boolean playTrack;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private FileTrackExporter fileTrackExporter;
private TrackExporter trackExporter;
// true if the AsyncTask has completed
private boolean completed;
@@ -75,13 +75,15 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackIds the track ids to save. To save all, set to size 1 with
* trackIds[0] == -1L
* @param directory the directory to save to
* @param playTrack true to play track
*/
public SaveAsyncTask(
SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds, File directory) {
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds,
File directory, boolean playTrack) {
this.saveActivity = saveActivity;
this.trackFileFormat = trackFileFormat;
this.trackIds = trackIds;
this.directory = directory;
this.playTrack = playTrack;
context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
@@ -164,8 +166,8 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onCancelled() {
if (fileTrackExporter != null) {
fileTrackExporter.stopWriteTrack();
if (trackExporter != null) {
trackExporter.stopWriteTrack();
}
}
@@ -179,16 +181,18 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
return false;
}
Track track = tracks[0];
boolean useKmz = !playTrack && trackFileFormat == TrackFileFormat.KML;
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = FileUtils.buildUniqueFileName(
directory, track.getName(), trackFileFormat.getExtension());
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
if (fileName == null) {
Log.d(TAG, "Unable to get a unique filename for " + track.getName());
return false;
}
fileTrackExporter = new FileTrackExporter(
context, myTracksProviderUtils, tracks, trackFileFormat, new TrackExporterListener() {
FileTrackExporter fileTrackExporter = new FileTrackExporter(myTracksProviderUtils, tracks,
trackFileFormat, context, useKmz, new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
@@ -202,24 +206,27 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
});
File file = null;
trackExporter = useKmz ? new KmzTrackExporter(myTracksProviderUtils, fileTrackExporter, tracks)
: fileTrackExporter;
File file = new File(directory, fileName);
FileOutputStream fileOutputStream;
try {
file = new File(directory, fileName);
OutputStream outputStream = new FileOutputStream(file);
fileTrackExporter.writeTrack(outputStream);
fileOutputStream = new FileOutputStream(file);
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found " + fileName, e);
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
}
trackExporter.writeTrack(fileOutputStream);
if (fileTrackExporter.isSuccess()) {
if (trackExporter.isSuccess()) {
savedPath = file.getAbsolutePath();
} else {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
}
return fileTrackExporter.isSuccess();
return trackExporter.isSuccess();
}
/**
@@ -12,25 +12,25 @@ import java.util.Locale;
public enum TrackFileFormat implements Parcelable {
KML {
@Override
TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context);
TrackWriter newTrackWriter(Context context, boolean inZip) {
return new KmlTrackWriter(context, inZip);
}
},
GPX {
@Override
TrackWriter newTrackWriter(Context context) {
TrackWriter newTrackWriter(Context context, boolean inZip) {
return new GpxTrackWriter(context);
}
},
CSV {
@Override
public TrackWriter newTrackWriter(Context context) {
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new CsvTrackWriter(context);
}
},
TCX {
@Override
public TrackWriter newTrackWriter(Context context) {
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new TcxTrackWriter(context);
}
};
@@ -60,7 +60,7 @@ public enum TrackFileFormat implements Parcelable {
/**
* Creates a new track writer for the format.
*/
abstract TrackWriter newTrackWriter(Context context);
abstract TrackWriter newTrackWriter(Context context, boolean inZip);
/**
* Returns the mime type for each format.
@@ -442,7 +442,7 @@ public class SyncUtils {
java.io.File file = new java.io.File(directory,
FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension()));
FileTrackExporter fileTrackExporter = new FileTrackExporter(
context, myTracksProviderUtils, new Track[] {track}, trackFileFormat, null);
myTracksProviderUtils, new Track[] { track }, trackFileFormat, context, false, null);
fileTrackExporter.writeTrack(new FileOutputStream(file));
if (fileTrackExporter.isSuccess()) {
@@ -54,7 +54,8 @@ public class KmlTrackWriterTest extends TrackWriterTest {
}
public void testXmlOutput() throws Exception {
KmlTrackWriter kmlTrackWriter = new KmlTrackWriter(getContext(), new FakeDescriptionGenerator());
KmlTrackWriter kmlTrackWriter = new KmlTrackWriter(
getContext(), false, new FakeDescriptionGenerator());
String result = writeTrack(kmlTrackWriter);
Document doc = parseXmlDocument(result);