/* * 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 "Colors.h" #include "Units.h" #include "TimeUtils.h" #include #include #include #include #include #include using namespace std; GoogleMapControl::GoogleMapControl(MainWindow *mw) : GcWindow(mw), main(mw), range(-1), current(NULL) { setInstanceName("Google Map"); setControls(NULL); setContentsMargins(0,0,0,0); layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(2,0,2,2); setLayout(layout); parent = mw; view = new QWebView(); view->setPage(new myWebPage()); view->setContentsMargins(0,0,0,0); view->page()->view()->setContentsMargins(0,0,0,0); view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(view); // we redraw on resize after a timeout // to let the user move splitter etc // without an immediate redraw, since it // makes the UI unresponsive delay = new QTimer(this); delay->setSingleShot(true); webBridge = new WebBridge(mw, this); //connect(parent, SIGNAL(rideSelected()), this, SLOT(rideSelected())); connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); connect(view->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(updateFrame())); connect(view, SIGNAL(loadStarted()), this, SLOT(loadStarted())); connect(view, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool))); connect(delay, SIGNAL(timeout()), this, SLOT(loadRide())); connect(mw, SIGNAL(intervalsChanged()), webBridge, SLOT(intervalsChanged())); connect(mw, SIGNAL(intervalSelected()), webBridge, SLOT(intervalsChanged())); first = true; loadingPage = false; newRideToLoad = false; } void GoogleMapControl::rideSelected() { // skip display if data drawn or invalid if (myRideItem == NULL || !amVisible()) return; RideItem * ride = myRideItem; if (ride == current || !ride || !ride->ride()) return; else current = ride; // Route metadata ... setSubTitle(ride->ride()->getTag("Route", "Route")); range =ride->zoneRange(); if(range < 0) rideCP = 300; // default cp to 300 watts else rideCP = ride->zones->getCP(range); newRideToLoad = true; loadRide(); } void GoogleMapControl::resizeEvent(QResizeEvent * ) { // XXX v3 API removes the need to do all this nonsense // since the map is enlarged as neccessary return; //XXX kept old code, just in case we decide // we still want to redraw on resize if (!amVisible()) return; if (first == true) { first = false; return; } newRideToLoad = true; delay->stop(); delay->start(1000); } void GoogleMapControl::loadStarted() { loadingPage = true; } /// called after the load is finished void GoogleMapControl::loadFinished(bool) { loadingPage = false; } void GoogleMapControl::loadRide() { if(loadingPage == true) return; if(newRideToLoad == true) { newRideToLoad = false; loadingPage = true; createHtml(); view->page()->mainFrame()->setHtml(currentPage); } } void GoogleMapControl::updateFrame() { view->page()->mainFrame()->addToJavaScriptWindowObject("webBridge", webBridge); } void GoogleMapControl::createHtml() { RideItem * ride = myRideItem; currentPage = ""; double minLat, minLon, maxLat, maxLon; minLat = minLon = 1000; maxLat = maxLon = -1000; // larger than 360 // get bounding co-ordinates for ride foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) { if (rfp->lat || rfp->lon) { minLat = std::min(minLat,rfp->lat); maxLat = std::max(maxLat,rfp->lat); minLon = std::min(minLon,rfp->lon); maxLon = std::max(maxLon,rfp->lon); } } // No GPS data, so sorry no map if(!ride || !ride->ride() || ride->ride()->areDataPresent()->lat == false || ride->ride()->areDataPresent()->lon == false) { currentPage = "No GPS Data Present"; return; } // load the Google Map v3 API currentPage = QString(" \n" "\n" "\n" " \n" "\n" "Golden Cheetah Map\n" " \n" " \n"); #if 0 // XXX overlay routine only works with MAPS api v2 // add in our library routines currentPage += createMapToolTipJavaScript(); #endif // local functions currentPage += QString("\n").arg(minLat,0,'g',6).arg(minLon,0,'g',6).arg(maxLat,0,'g',6).arg(maxLon,0,'g',6); // the main page is rather trivial currentPage += QString("\n" "\n" "
\n" "\n" "\n"); } QColor GoogleMapControl::GetColor(int watts) { if (range < 0) return Qt::red; else return zoneColor(main->zones()->whichZone(range, watts), 7); } /// create the ride line void GoogleMapControl::drawShadedRoute() { int intervalTime = 60; // 60 seconds double rtime=0; // running total for accumulated data int count=0; // how many samples ? int rwatts=0; // running total of watts double prevtime=0; // time for previous point QString code; foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) { if (count == 0) { code = QString("{\nvar polyline = new google.maps.Polyline();\n" " polyline.setMap(map);\n" " path = polyline.getPath();\n"); } else { if (rfp->lat || rfp->lon) code += QString("path.push(new google.maps.LatLng(%1,%2));\n").arg(rfp->lat,0,'g',6).arg(rfp->lon,0,'g',6); } // running total of time rtime += rfp->secs - prevtime; rwatts += rfp->watts; prevtime = rfp->secs; count++; // end of segment if (rtime >= intervalTime) { int avgWatts = rwatts / count; QColor color = GetColor(avgWatts); // thats this segment done, so finish off and // add tooltip junk count = rwatts = rtime = 0; // color the polyline code += QString("var polyOptions = {\n" " strokeColor: '%1',\n" " strokeWeight: 3,\n" " strokeOpacity: 0.5,\n" // for out and backs, we need both " zIndex: 0,\n" "}\n" "polyline.setOptions(polyOptions);\n" #if 0 //XXX map.{add,remove}Overlay is deprecated.. // tooltip "google.maps.event.addListener(polyline, 'mouseover', function() {\n" " var tooltip_text = '30s Power: %2';\n" " this.overlay = new MapTooltip(this,tooltip_text);\n" " map.addOverlay(this.overlay);\n" "});\n" "google.maps.event.addListener(polyline, 'mouseout', function() {\n" " map.removeOverlay(this.overlay);\n" "});\n" #endif "}\n").arg(color.name()); #if 0 //XXX map.{add,remove}Overlay is deprecated.. .arg(avgWatts); #endif view->page()->mainFrame()->evaluateJavaScript(code); } } } // // Static helper - havervine formaula for calculating the distance // between 2 geo co-ordinates // static const double DEG_TO_RAD = 0.017453292519943295769236907684886; static const double EARTH_RADIUS_IN_METERS = 6372797.560856; static double ArcInRadians(double fromLat, double fromLon, double toLat, double toLon) { double latitudeArc = (fromLat - toLat) * DEG_TO_RAD; double longitudeArc = (fromLon - toLon) * DEG_TO_RAD; double latitudeH = sin(latitudeArc * 0.5); latitudeH *= latitudeH; double lontitudeH = sin(longitudeArc * 0.5); lontitudeH *= lontitudeH; double tmp = cos(fromLat*DEG_TO_RAD) * cos(toLat*DEG_TO_RAD); return 2.0 * asin(sqrt(latitudeH + tmp*lontitudeH)); } static double distanceBetween(double fromLat, double fromLon, double toLat, double toLon) { return EARTH_RADIUS_IN_METERS*ArcInRadians(fromLat, fromLon, toLat, toLon); } void GoogleMapControl::createMarkers() { QString code; // // START / FINISH MARKER // const QVector &points = myRideItem->ride()->dataPoints(); bool loop = distanceBetween(points[0]->lat, points[0]->lon, points[points.count()-1]->lat, points[points.count()-1]->lon) < 100 ? true : false; if (loop) { code = QString("{ var latlng = new google.maps.LatLng(%1,%2);" "var image = new google.maps.MarkerImage('qrc:images/maps/loop.png');" "var marker = new google.maps.Marker({ icon: image, animation: google.maps.Animation.DROP, position: latlng });" "marker.setMap(map); }").arg(points[0]->lat,0,'g',6).arg(points[0]->lon,0,'g',6); view->page()->mainFrame()->evaluateJavaScript(code); } else { // start / finish markers code = QString("{ var latlng = new google.maps.LatLng(%1,%2);" "var image = new google.maps.MarkerImage('qrc:images/maps/cycling.png');" "var marker = new google.maps.Marker({ icon: image, animation: google.maps.Animation.DROP, position: latlng });" "marker.setMap(map); }").arg(points[0]->lat,0,'g',6).arg(points[0]->lon,0,'g',6); view->page()->mainFrame()->evaluateJavaScript(code); code = QString("{ var latlng = new google.maps.LatLng(%1,%2);" "var image = new google.maps.MarkerImage('qrc:images/maps/finish.png');" "var marker = new google.maps.Marker({ icon: image, animation: google.maps.Animation.DROP, position: latlng });" "marker.setMap(map); }").arg(points[points.count()-1]->lat,0,'g',6).arg(points[points.count()-1]->lon,0,'g',6); view->page()->mainFrame()->evaluateJavaScript(code); } // // STOPS - BEER AND BURRITO TIME (> 5 mins in same spot) // double stoplat=0, stoplon=0; double laststoptime=0; double lastlat=0, lastlon=0; int stoptime=0; static const int BEERANDBURRITO = 300; // anything longer than 5 minutes foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) { if (!rfp->lat || !rfp->lon) continue; // ignore blank values if (!stoplat || !stoplon) { // register first gps co-ord stoplat = rfp->lat; stoplon = rfp->lon; laststoptime = rfp->secs; } if (distanceBetween(rfp->lat, rfp->lon, stoplat, stoplon) < 20) { if (rfp->secs - laststoptime > myRideItem->ride()->recIntSecs()) stoptime += rfp->secs - laststoptime; else stoptime += myRideItem->ride()->recIntSecs(); } else if (rfp->secs - laststoptime > myRideItem->ride()->recIntSecs()) { stoptime += rfp->secs - laststoptime; stoplat = rfp->lat; stoplon = rfp->lon; } else { stoptime = 0; stoplat = rfp->lat; stoplon = rfp->lon; } if (stoptime > BEERANDBURRITO) { // 3 minutes is more than a traffic light stop dude. if ((!lastlat && !lastlon) || distanceBetween(lastlat, lastlon, stoplat, stoplon)>100) { lastlat = stoplat; lastlon = stoplon; code = QString( "{ var latlng = new google.maps.LatLng(%1,%2);" "var image = new google.maps.MarkerImage('qrc:images/maps/cycling_feed.png');" "var marker = new google.maps.Marker({ icon: image, animation: google.maps.Animation.DROP, position: latlng });" "marker.setMap(map);" "}").arg(rfp->lat,0,'g',6).arg(rfp->lon,0,'g',6); view->page()->mainFrame()->evaluateJavaScript(code); stoptime=0; } stoplat=stoplon=stoptime=0; } laststoptime = rfp->secs; } // // INTERVAL MARKERS // if (main->allIntervalItems() == NULL) return; // none to do, we are all done then int interval=0; foreach (const RideFileInterval x, myRideItem->ride()->intervals()) { int offset = myRideItem->ride()->intervalBegin(x); code = QString( "{" " var latlng = new google.maps.LatLng(%1,%2);" " var marker = new google.maps.Marker({ title: '%3', animation: google.maps.Animation.DROP, position: latlng });" " marker.setMap(map);" " markerList.push(marker);" // keep track of those suckers " google.maps.event.addListener(marker, 'click', function(event) { webBridge.toggleInterval(%4); });" "}") .arg(myRideItem->ride()->dataPoints()[offset]->lat,0,'g',6) .arg(myRideItem->ride()->dataPoints()[offset]->lon,0,'g',6) .arg(x.name) .arg(interval); view->page()->mainFrame()->evaluateJavaScript(code); interval++; } return; } QString GoogleMapControl::createMapToolTipJavaScript() { return("\n"); } // quick diag, used to debug code only void WebBridge::call(int count) { qDebug()<<"webBridge call:"<property("ride").value(); if (mainWindow->allIntervalItems() == NULL || rideItem == NULL || rideItem->ride() == NULL) return 0; // not inited yet! for (int i=0; iallIntervalItems()->childCount(); i++) { IntervalItem *current = dynamic_cast(mainWindow->allIntervalItems()->child(i)); if (current != NULL) { if (current->isSelected() == true) { ++highlighted; } } } return highlighted; } // get a latlon array for the i'th selected interval QVariantList WebBridge::getLatLons(int i) { QVariantList latlons; int highlighted=0; RideItem *rideItem = gm->property("ride").value(); if (mainWindow->allIntervalItems() == NULL || rideItem ==NULL || rideItem->ride() == NULL) return latlons; // not inited yet! if (i) { // get for specific interval for (int j=0; jallIntervalItems()->childCount(); j++) { IntervalItem *current = dynamic_cast(mainWindow->allIntervalItems()->child(j)); if (current != NULL) { if (current->isSelected() == true) { ++highlighted; // this one! if (highlighted==i) { // so this one is the interval we need.. lets // snaffle up the points in this section foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) { if (p1->secs+rideItem->ride()->recIntSecs() > current->start && p1->secs< current->stop) { if (p1->lat || p1->lon) { latlons << p1->lat; latlons << p1->lon; } } } return latlons; } } } } } else { // get latlons for entire route foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) { if (p1->lat || p1->lon) { latlons << p1->lat; latlons << p1->lon; } } } return latlons; } // once the basic map and route have been marked, overlay markers, shaded areas etc void WebBridge::drawOverlays() { // overlay the markers gm->createMarkers(); // overlay a shaded route gm->drawShadedRoute(); } // interval marker was clicked on the map, toggle its display void WebBridge::toggleInterval(int x) { IntervalItem *current = dynamic_cast(mainWindow->allIntervalItems()->child(x)); if (current) current->setSelected(!current->isSelected()); }