/* * Copyright (c) 2009 Greg Lonnon (greg.lonnon@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 "GoogleMapControl.h" #include "RideItem.h" #include "RideFile.h" #include "MainWindow.h" #include "Zones.h" #include "Settings.h" #include "Units.h" #include #include #include #include #include #include #include #include using namespace std; namespace gm { // quick ideas on a math pipeline, kindof like this... // but more of a pipeline... // it makes the math somewhat easier to do and understand... class RideFilePointAlgorithm { protected: RideFilePoint prevRfp; bool first; RideFilePointAlgorithm() { first = false; } }; // Algorithm to find the meidan of a set of data template class Median { boost::circular_buffer buffer; public: Median(int size) { buffer.set_capacity(size); } // add the new data void add(T a) { buffer.push_back(a); } operator T() { if(buffer.size() == 0) { return 0; } T total = 0; BOOST_FOREACH(T point, buffer) { total += point; } return total / buffer.size(); } }; class AltGained : private RideFilePointAlgorithm { protected: double gained; double curAlt, prevAlt; Median median; public: AltGained(): gained(0), curAlt(0), prevAlt(0), median(20) {} void operator()(RideFilePoint rfp) { median.add(rfp.alt); curAlt = median; if(prevAlt == 0) { prevAlt = median; } if(curAlt> prevAlt) { gained += curAlt - prevAlt; } prevAlt = curAlt; } int TotalGained() { return gained; } operator int() { return TotalGained(); } }; class AvgHR { int samples; int totalHR; public: AvgHR() : samples(0),totalHR(0.0) {} void operator()(RideFilePoint rfp) { totalHR += rfp.hr; samples++; } int HR() { return totalHR / samples; } operator int() { return HR(); } }; class AvgPower { int samples; double totalPower; public: AvgPower() : samples(0), totalPower(0.0) { } void operator()(RideFilePoint rfp) { totalPower += rfp.watts; samples++; } int Power() { return (int) (totalPower / samples); } operator int() { return Power(); } }; } using namespace gm; #define GOOGLE_KEY "ABQIAAAAS9Z2oFR8vUfLGYSzz40VwRQ69UCJw2HkJgivzGoninIyL8-QPBTtnR-6pM84ljHLEk3PDql0e2nJmg" GoogleMapControl::GoogleMapControl(MainWindow *mw) : main(mw), range(-1), current(NULL) { parent = mw; view = new QWebView(); layout = new QVBoxLayout(); layout->addWidget(view); setLayout(layout); connect(parent, SIGNAL(rideSelected()), this, SLOT(rideSelected())); connect(view, SIGNAL(loadStarted()), this, SLOT(loadStarted())); connect(view, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool))); loadingPage = false; newRideToLoad = false; } void GoogleMapControl::rideSelected() { if (parent->activeTab() != this) return; RideItem * ride = parent->rideItem(); if (ride == current || !ride || !ride->ride()) return; else current = ride; range =ride->zoneRange(); if(range < 0) { rideCP = 300; // default cp to 300 watts } else { rideCP = ride->zones->getCP(range); } rideData.clear(); double prevLon = 1000; double prevLat = 1000; foreach(RideFilePoint *rfp,ride->ride()->dataPoints()) { RideFilePoint curRfp = *rfp; // wko imports can have -320 type of values for roughly 40 degrees // This if range is a guess that we only have these -ve type numbers for actual 0 to 90 deg of Latitude if(curRfp.lat <= -270 && curRfp.lat >= -360) { curRfp.lat = 360 + curRfp.lat; } // Longitude = -180 to 180 degrees, Latitude = -90 to 90 degrees - anything else is invalid. if((curRfp.lon < -180 || curRfp.lon > 180 || curRfp.lat < -90 || curRfp.lat > 90) // Garmin FIT records a missed GPS signal as 0/0. || ((curRfp.lon == 0) && (curRfp.lat == 0))) { curRfp.lon = 999; curRfp.lat = 999; } prevLon = curRfp.lon; prevLat = curRfp.lat; rideData.push_back(curRfp); } newRideToLoad = true; loadRide(); } void GoogleMapControl::resizeEvent(QResizeEvent * ) { static bool first = true; if (parent->activeTab() != this) return; if (first == true) { first = false; } else { newRideToLoad = true; loadRide(); } } void GoogleMapControl::loadStarted() { loadingPage = true; } /// called after the load is finished void GoogleMapControl::loadFinished(bool) { loadingPage = false; loadRide(); } void GoogleMapControl::loadRide() { if(loadingPage == true) { return; } if(newRideToLoad == true) { createHtml(); newRideToLoad = false; loadingPage = true; QString htmlFile(QDir::tempPath()); htmlFile.append("/maps.html"); QFile file(htmlFile); file.remove(); file.open(QIODevice::ReadWrite); file.write(currentPage.str().c_str(),currentPage.str().length()); file.flush(); file.close(); QString filename("file:///"); filename.append(htmlFile); QUrl url(filename); view->load(url); } } void GoogleMapControl::createHtml() { currentPage.str(""); double minLat, minLon, maxLat, maxLon; minLat = minLon = 1000; maxLat = maxLon = -1000; // larger than 360 BOOST_FOREACH(RideFilePoint rfp, rideData) { if (rfp.lat != 999 && rfp.lon != 999) { minLat = std::min(minLat,rfp.lat); maxLat = std::max(maxLat,rfp.lat); minLon = std::min(minLon,rfp.lon); maxLon = std::max(maxLon,rfp.lon); } } if(minLon == 1000) { currentPage << tr("No GPS Data Present").toStdString(); return; } /// seems to be the magic number... to stop the scrollbars int width = view->width() -16; int height = view->height() -16; std::ostringstream oss; oss.precision(6); oss.setf(ios::fixed,ios::floatfield); oss << "" << endl << "" << endl << "" << endl << "" << endl << "GoldenCheetah" << endl << "" << endl << CreateMapToolTipJavaScript() << endl << "" << endl << "" << endl << "" << endl << "
" << endl << "
" << endl << "
" << endl << "" << endl << "" << endl; currentPage << oss.str(); } QColor GoogleMapControl::GetColor(int watts) { if (range < 0) return Qt::red; else return zoneColor(main->zones()->whichZone(range, watts), 7); } /// create the ride line string GoogleMapControl::CreatePolyLine() { std::vector intervalPoints; ostringstream oss; int intervalTime = 30; // 30 seconds BOOST_FOREACH(RideFilePoint rfp, rideData) { intervalPoints.push_back(rfp); if((intervalPoints.back().secs - intervalPoints.front().secs) > intervalTime) { // find the avg power and color code it and create a polyline... AvgPower avgPower = for_each(intervalPoints.begin(), intervalPoints.end(), AvgPower()); // find the color // create the polyline CreateSubPolyLine(intervalPoints,oss,avgPower); intervalPoints.clear(); intervalPoints.push_back(rfp); } } return oss.str(); } void GoogleMapControl::CreateSubPolyLine(const std::vector &points, std::ostringstream &oss, int avgPower) { oss.precision(6); QColor color = GetColor(avgPower); QString colorstr = color.name(); oss.setf(ios::fixed,ios::floatfield); oss << "var polyline = new GPolyline(["; BOOST_FOREACH(RideFilePoint rfp, points) { if (!(rfp.lat == 999 && rfp.lon == 999)) { oss << "new GLatLng(" << rfp.lat << "," << rfp.lon << ")," << endl; } } oss << "],\"" << colorstr.toStdString() << "\",4);" << endl; oss << "GEvent.addListener(polyline, 'mouseover', function() {" << endl << "var tooltip_text = '30s Power: " << avgPower << "';" << endl << "var ss={'weight':8};" << endl << "this.setStrokeStyle(ss);" << endl << "this.overlay = new MapTooltip(this,tooltip_text);" << endl << "map.addOverlay(this.overlay);" << endl << "});" << endl << "GEvent.addListener(polyline, 'mouseout', function() {" << endl << "map.removeOverlay(this.overlay);" << endl << "var ss={'weight':5};" << endl << "this.setStrokeStyle(ss);" << endl << "});" << endl; oss << "map.addOverlay (polyline);" << endl; } string GoogleMapControl::CreateIntervalMarkers() { bool useMetricUnits = (GetApplicationSettings()->value(GC_UNIT).toString() == "Metric"); ostringstream oss; oss.precision(6); oss.setf(ios::fixed,ios::floatfield); oss << "var marker;" << endl; int currentInterval = 0; std::vector intervalPoints; BOOST_FOREACH(RideFilePoint rfp, rideData) { intervalPoints.push_back(rfp); if(currentInterval < rfp.interval) { // want to see avg power, avg speed, alt changes, avg hr double distance = intervalPoints.back().km - intervalPoints.front().km ; distance = (useMetricUnits ? distance : distance * MILES_PER_KM); int secs = intervalPoints.back().secs - intervalPoints.front().secs; double avgSpeed = (distance/((double)secs)) * 3600; QTime time; time = time.addSecs(secs); AvgHR avgHr = for_each(intervalPoints.begin(), intervalPoints.end(), AvgHR()); AvgPower avgPower = for_each(intervalPoints.begin(), intervalPoints.end(), AvgPower()); int altGained =ceil(for_each(intervalPoints.begin(), intervalPoints.end(), AltGained())); altGained = ceil((useMetricUnits ? altGained : altGained * FEET_PER_METER)); oss.precision(6); oss << "marker = new GMarker(new GLatLng( "; oss<< rfp.lat << "," << rfp.lon << "));" << endl; oss << "marker.bindInfoWindowHtml(" <

Lap: " << currentInterval << "

" ; oss.precision(1); oss << "

Distance: " << distance << " " << (useMetricUnits ? "KM" : "Miles") << "

" ; oss << "

Time: " << time.toString().toStdString() << "

"; oss << "

Avg Speed: " << avgSpeed << " " << (useMetricUnits ? tr("KPH") : tr("MPH")).toStdString() << "

"; if(avgHr != 0) { oss << "

Avg HR: " << avgHr << "

"; } if(avgPower != 0) { oss << "

Avg Power: " << avgPower << " Watts

"; } oss << "

Alt Gained: " << altGained << " " << (useMetricUnits ? tr("Meters") : tr("Feet")).toStdString() << "

"; oss << "\");" << endl; oss << "map.addOverlay(marker);" << endl; currentInterval = rfp.interval; intervalPoints.clear(); } } return oss.str(); } string GoogleMapControl::CreateMapToolTipJavaScript() { ostringstream oss; oss << " "<< endl; return oss.str(); }