From f6eb97ec0f09dbd221e0c7e60eddeda4e5c66e75 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Thu, 26 Aug 2010 10:47:49 +0100 Subject: [PATCH] Add support for Google Earth (KML) This patch adds an 'Export to KML' option to the ride menu. It will create a .kml file including power, hr, torque etc. These can be viewed alongside the map view in Google Earth 5.2. Please note this requires libkml. The features of libkml that are required were introduced in revision 852 which means that as of Aug 2010 you will need to checkout from the SVN source repo and build; svn checkout http://libkml.googlecode.com/svn/trunk/ libkml-read-only and the ./configure mantra that worked successfully for me on Mac OS X was; ./configure CC="gcc -arch i386" CXX="g++ -arch i386" --disable-swig Building on WIN32 is currently fraught with issues, unless you build via MSVC 2010. Linux is straight forward but you will need to install / apt-get libcurl. Fixes #133. --- src/KmlRideFile.cpp | 305 ++++++++++++++++++++++++++++++++++++++++++++ src/KmlRideFile.h | 29 +++++ src/MainWindow.cpp | 29 +++++ src/MainWindow.h | 3 + src/gcconfig.pri.in | 12 ++ src/src.pro | 13 ++ 6 files changed, 391 insertions(+) create mode 100644 src/KmlRideFile.cpp create mode 100644 src/KmlRideFile.h diff --git a/src/KmlRideFile.cpp b/src/KmlRideFile.cpp new file mode 100644 index 000000000..d91bc5d6e --- /dev/null +++ b/src/KmlRideFile.cpp @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include "KmlRideFile.h" +#include + +#include +#include +#include +#include "boost/scoped_ptr.hpp" +#include "kml/base/date_time.h" +#include "kml/base/expat_parser.h" +#include "kml/base/file.h" +#include "kml/base/vec3.h" +#include "kml/convenience/convenience.h" +#include "kml/convenience/gpx_trk_pt_handler.h" +#include "kml/dom.h" +#include "kml/dom/kml_ptr.h" + +// majority of code swiped from the libkml example gpx2kml.cc +using kmlbase::ExpatParser; +using kmlbase::DateTime; +using kmlbase::Vec3; +using kmlbase::Attributes; +using kmldom::ContainerPtr; +using kmldom::FolderPtr; +using kmldom::IconStylePtr; +using kmldom::IconStyleIconPtr; +using kmldom::KmlFactory; +using kmldom::KmlPtr; +using kmldom::LabelStylePtr; +using kmldom::ListStylePtr; +using kmldom::PairPtr; +using kmldom::PointPtr; +using kmldom::SchemaPtr; +using kmldom::ExtendedDataPtr; +using kmldom::SchemaDataPtr; +using kmldom::GxSimpleArrayFieldPtr; +using kmldom::GxSimpleArrayDataPtr; +using kmldom::GxMultiTrackPtr; +using kmldom::GxTrackPtr; +using kmldom::PlacemarkPtr; +using kmldom::TimeStampPtr; +using kmldom::StylePtr; +using kmldom::StyleMapPtr; + +// +// Utility functions +// +static const char kDotIcon[] = + "http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png"; + + +static ExtendedDataPtr CreateExtendedData() { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + ExtendedDataPtr ed = kml_factory->CreateExtendedData(); + return ed; +} + +static SchemaDataPtr CreateSchemaData(string name) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + SchemaDataPtr sd = kml_factory->CreateSchemaData(); + sd->set_schemaurl("#" + name); + return sd; +} + +static GxSimpleArrayDataPtr CreateGxSimpleArrayData(string name) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + GxSimpleArrayDataPtr sa = kml_factory->CreateGxSimpleArrayData(); + sa->set_name(name); + return sa; +} + +static PlacemarkPtr CreateGxTrackPlacemark(string name, GxTrackPtr tracks) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + + PlacemarkPtr placemark = kml_factory->CreatePlacemark(); + placemark->set_name(name); + placemark->set_id(name); + + placemark->set_geometry(tracks); + return placemark; +} + +static GxTrackPtr CreateGxTrack(string name) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + GxTrackPtr track = kml_factory->CreateGxTrack(); + track->set_id(name); + return track; +} + +static SchemaPtr CreateSchema(string name) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + SchemaPtr schema = kml_factory->CreateSchema(); + schema->set_id(name); + schema->set_name(name); + return schema; +} + +static GxSimpleArrayFieldPtr CreateGxSimpleArrayField(string name) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + GxSimpleArrayFieldPtr field = kml_factory->CreateGxSimpleArrayField(); + field->set_type("float"); + field->set_name(name); + field->set_displayname(name); + return field; +} + +static IconStylePtr CreateIconStyle(double scale) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + IconStyleIconPtr icon = kml_factory->CreateIconStyleIcon(); + icon->set_href(kDotIcon); + IconStylePtr icon_style = kml_factory->CreateIconStyle(); + icon_style->set_icon(icon); + icon_style->set_scale(scale); + return icon_style; +} + +static LabelStylePtr CreateLabelStyle(double scale) { + LabelStylePtr label_style = KmlFactory::GetFactory()->CreateLabelStyle(); + label_style->set_scale(scale); + return label_style; +} + +static PairPtr CreatePair(int style_state, double icon_scale) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + PairPtr pair = kml_factory->CreatePair(); + pair->set_key(style_state); + StylePtr style = kml_factory->CreateStyle(); + style->set_iconstyle(CreateIconStyle(icon_scale)); + // Hide the label in normal style state, visible in highlight. + style->set_labelstyle(CreateLabelStyle( + style_state == kmldom::STYLESTATE_NORMAL ? 0 : 1 )); + pair->set_styleselector(style); + return pair; +} + +static StylePtr CreateRadioFolder(const char* id) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + ListStylePtr list_style = kml_factory->CreateListStyle(); + list_style->set_listitemtype(kmldom::LISTITEMTYPE_RADIOFOLDER); + StylePtr style = kml_factory->CreateStyle(); + style->set_liststyle(list_style); + style->set_id(id); + return style; +} + +static StyleMapPtr CreateStyleMap(const char* id) { + KmlFactory* kml_factory = KmlFactory::GetFactory(); + StyleMapPtr style_map = kml_factory->CreateStyleMap(); + style_map->set_id(id); + style_map->add_pair(CreatePair(kmldom::STYLESTATE_NORMAL, 0.1)); + style_map->add_pair(CreatePair(kmldom::STYLESTATE_HIGHLIGHT, 0.3)); + return style_map; +} + +// +// Serialise the ride +// +bool +KmlFileReader::writeRideFile(const RideFile * ride, QFile &file) const +{ + // Create a new DOM document and setup styles et al + kmldom::KmlFactory* kml_factory = kmldom::KmlFactory::GetFactory(); + kmldom::DocumentPtr document = kml_factory->CreateDocument(); + const char* kRadioFolderId = "radio-folder-style"; + document->add_styleselector(CreateRadioFolder(kRadioFolderId)); + document->set_styleurl(std::string("#") + kRadioFolderId); + const char* kStyleMapId = "style-map"; + document->add_styleselector(CreateStyleMap(kStyleMapId)); + document->set_name("Golden Cheetah"); + + // add the schema elements for each data series + SchemaPtr schemadef = CreateSchema("schema"); + // gx:SimpleArrayField ... + if (ride->areDataPresent()->cad) + schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("cadence")); + if (ride->areDataPresent()->hr) + schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("heartrate")); + if (ride->areDataPresent()->watts) + schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("power")); + if (ride->areDataPresent()->nm) + schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("torque")); + if (ride->areDataPresent()->headwind) + schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("headwind")); + document->add_schema(schemadef); + + // setup trip folder (shown on lhs of google earth + FolderPtr folder = kmldom::KmlFactory::GetFactory()->CreateFolder(); + folder->set_name("Bike Rides"); + document->add_feature(folder); + + // Create a track for the entire ride + GxTrackPtr track = CreateGxTrack("Entire Ride"); + PlacemarkPtr placemark = CreateGxTrackPlacemark(QString("Bike %1") + .arg(ride->startTime().toString()).toStdString(), track); + folder->add_feature(placemark); + + // + // Basic Data -- Lat/Lon/Alt and Timestamp + // + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + + // lots of arsing around with dates XXX clean this up + QDateTime timestamp(ride->startTime().addSecs(datapoint->secs)); + string stdctimestamp = timestamp.toString(Qt::ISODate).toStdString() + "Z"; //<< 'Z' fixes crash! + kmlbase::DateTime *when = kmlbase::DateTime::Create(stdctimestamp.data()); + if (datapoint->lat && datapoint->lon) track->add_when(when->GetXsdDateTime()); + } + + // loop through the entire ride + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) track->add_gx_coord(kmlbase::Vec3(datapoint->lon, datapoint->lat, datapoint->alt)); + } + + // + // Extended Data -- cadence, heartrate, power, torque, headwind + // + ExtendedDataPtr extended = CreateExtendedData(); + track->set_extendeddata(extended); + SchemaDataPtr schema = CreateSchemaData("schema"); + extended->add_schemadata(schema); + + // power + if (ride->areDataPresent()->watts) { + GxSimpleArrayDataPtr power = CreateGxSimpleArrayData("power"); + schema->add_gx_simplearraydata(power); + + // now create a GxSimpleArrayData + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) power->add_gx_value(QString("%1").arg(datapoint->watts).toStdString()); + } + } + // cadence + if (ride->areDataPresent()->cad) { + GxSimpleArrayDataPtr cadence = CreateGxSimpleArrayData("cadence"); + schema->add_gx_simplearraydata(cadence); + + // now create a GxSimpleArrayData + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) cadence->add_gx_value(QString("%1").arg(datapoint->cad).toStdString()); + } + } + // heartrate + if (ride->areDataPresent()->hr) { + GxSimpleArrayDataPtr heartrate = CreateGxSimpleArrayData("heartrate"); + schema->add_gx_simplearraydata(heartrate); + + // now create a GxSimpleArrayData + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) heartrate->add_gx_value(QString("%1").arg(datapoint->hr).toStdString()); + } + } + // torque + if (ride->areDataPresent()->nm) { + GxSimpleArrayDataPtr torque = CreateGxSimpleArrayData("torque"); + schema->add_gx_simplearraydata(torque); + + // now create a GxSimpleArrayData + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) torque->add_gx_value(QString("%1").arg(datapoint->nm).toStdString()); + } + } + // headwind + if (ride->areDataPresent()->headwind) { + GxSimpleArrayDataPtr headwind = CreateGxSimpleArrayData("headwind"); + schema->add_gx_simplearraydata(headwind); + + // now create a GxSimpleArrayData + foreach (RideFilePoint *datapoint, ride->dataPoints()) { + if (datapoint->lat && datapoint->lon) headwind->add_gx_value(QString("%1").arg(datapoint->headwind).toStdString()); + } + } + + // Create KML document + kmldom::KmlPtr kml = kml_factory->CreateKml(); + kml->set_feature(document); + + // make sure the google extensions are added in! + kmlbase::Attributes gxxmlns22; + gxxmlns22.SetValue("gx", "http://www.google.com/kml/ext/2.2"); + kml->MergeXmlns(gxxmlns22); + + // Serialize + if (!file.open(QIODevice::WriteOnly)) return(false); + file.write(kmldom::SerializePretty(kml).data()); + file.close(); + return(true); +} diff --git a/src/KmlRideFile.h b/src/KmlRideFile.h new file mode 100644 index 000000000..abc983318 --- /dev/null +++ b/src/KmlRideFile.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _KmlRideFile_h +#define _KmlRideFile_h + +#include "RideFile.h" + +struct KmlFileReader : public RideFileReader { + virtual RideFile *openRideFile(QFile &, QStringList &) const { return NULL; } // does not support reading + virtual bool writeRideFile(const RideFile *ride, QFile &file) const; +}; + +#endif // _KmlRideFile_h diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6517bda65..62412f9aa 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -29,6 +29,9 @@ #include "ConfigDialog.h" #include "CriticalPowerWindow.h" #include "GcRideFile.h" +#ifdef GC_HAVE_KML +#include "KmlRideFile.h" +#endif #include "PwxRideFile.h" #include "LTMWindow.h" #include "PfPvWindow.h" @@ -374,6 +377,10 @@ MainWindow::MainWindow(const QDir &home) : SLOT(exportCSV()), tr("Ctrl+E")); rideMenu->addAction(tr("Export to GC..."), this, SLOT(exportGC())); +#ifdef GC_HAVE_KML + rideMenu->addAction(tr("&Export to KML..."), this, + SLOT(exportKML())); +#endif rideMenu->addAction(tr("Export to PWX..."), this, SLOT(exportPWX())); rideMenu->addSeparator (); @@ -713,6 +720,28 @@ MainWindow::exportGC() reader.writeRideFile(currentRide(), file); } +#ifdef GC_HAVE_KML +void +MainWindow::exportKML() +{ + if ((treeWidget->selectedItems().size() != 1) + || (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) { + QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!")); + return; + } + + QString fileName = QFileDialog::getSaveFileName( + this, tr("Export KML"), QDir::homePath(), tr("Google Earth KML (*.kml)")); + if (fileName.length() == 0) + return; + + QString err; + QFile file(fileName); + KmlFileReader reader; + reader.writeRideFile(currentRide(), file); +} +#endif + void MainWindow::exportCSV() { diff --git a/src/MainWindow.h b/src/MainWindow.h index aa2be7544..299b01551 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -132,6 +132,9 @@ class MainWindow : public QMainWindow void exportPWX(); void exportCSV(); void exportGC(); +#ifdef GC_HAVE_KML + void exportKML(); +#endif void manualProcess(QString); void importFile(); void findBestIntervals(); diff --git a/src/gcconfig.pri.in b/src/gcconfig.pri.in index 83e4d6792..934be863e 100644 --- a/src/gcconfig.pri.in +++ b/src/gcconfig.pri.in @@ -14,6 +14,18 @@ D2XX_INCLUDE = /usr/local/include/D2XX #QWT3D_INSTALL = /usr/local/qwtplot3d +# If you want support for Google Earth .kml files then you need +# to install the Google libkml library +# +# http://code.google.com/p/libkml/ +# or on Linux sudo apt-get install libkml-dev +# +# then set the following variable appropriately +# to the root of the libs/includ path + +#KML_INSTALL = /usr/local + + # We recommend a debug build for development, and a static build for releases. CONFIG += debug #CONFIG += static diff --git a/src/src.pro b/src/src.pro index 6780d7530..15b85502d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -41,6 +41,19 @@ qwt3d { DEFINES += GC_HAVE_QWTPLOT3D } +!isEmpty( KML_INSTALL) { + KML_INCLUDE = $${KML_INSTALL}/include/kml + KML_LIBS = $${KML_INSTALL}/lib/libkmldom.a \ + $${KML_INSTALL}/lib/libkmlconvenience.a \ + $${KML_INSTALL}/lib/libkmlengine.a \ + $${KML_INSTALL}/lib/libkmlbase.a \ + + LIBS += $${KML_LIBS} $${KML_LIBS} + DEFINES += GC_HAVE_KML + SOURCES += KmlRideFile.cpp + HEADERS += KmlRideFile.h +} + macx { LIBS += -framework Carbon }