/* * Copyright (c) 2009 Greg Lonnon (greg.lonnon@gmail.com) * 2011 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 "GoogleMapControl.h" #include "MainWindow.h" #include "RideItem.h" #include "RideFile.h" #include "IntervalItem.h" #include "IntervalTreeView.h" #include "Context.h" #include "Athlete.h" #include "Zones.h" #include "Settings.h" #include "Colors.h" #include "Units.h" #include "TimeUtils.h" #include "HelpWhatsThis.h" // overlay helper #include "TabView.h" #include "GcOverlayWidget.h" #include "IntervalSummaryWindow.h" #include GoogleMapControl::GoogleMapControl(Context *context) : GcChartWindow(context), context(context), range(-1), current(NULL), firstShow(true), stale(false) { setControls(NULL); setContentsMargins(0,0,0,0); layout = new QVBoxLayout(); layout->setSpacing(0); layout->setContentsMargins(2,0,2,2); setChartLayout(layout); 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); view->setAcceptDrops(false); layout->addWidget(view); HelpWhatsThis *help = new HelpWhatsThis(view); view->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartRides_Map)); webBridge = new WebBridge(context, this); // put a helper on the screen for mouse over intervals... overlayIntervals = new IntervalSummaryWindow(context); addHelper(tr("Intervals"), overlayIntervals); // // connects // connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); connect(view->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(updateFrame())); connect(context, SIGNAL(rideChanged(RideItem*)), this, SLOT(forceReplot())); connect(context, SIGNAL(intervalsChanged()), webBridge, SLOT(intervalsChanged())); connect(context, SIGNAL(intervalSelected()), webBridge, SLOT(intervalsChanged())); connect(context, SIGNAL(intervalZoom(IntervalItem*)), this, SLOT(zoomInterval(IntervalItem*))); connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32))); first = true; configChanged(CONFIG_APPEARANCE); } GoogleMapControl::~GoogleMapControl() { delete webBridge; } void GoogleMapControl::configChanged(qint32) { setProperty("color", GColor(CPLOTBACKGROUND)); #ifndef Q_OS_MAC overlayIntervals->setStyleSheet(TabView::ourStyleSheet()); #endif } void GoogleMapControl::forceReplot() { stale=true; rideSelected(); } void GoogleMapControl::rideSelected() { RideItem * ride = myRideItem; // set/unset blank then decide what to do next if (!ride || !ride->ride() || !ride->ride()->dataPoints().count()) setIsBlank(true); else setIsBlank(false); // skip display if data already drawn or invalid if (myRideItem == NULL || !amVisible()) return; // nothing to plot if (!ride || !ride->ride()) return; else if (!stale && ride == current) return; // remember what we last plotted current = ride; // Route metadata ... setSubTitle(ride->ride()->getTag("Route", tr("Route"))); // default to .. range = -1; rideCP = 300; stale = false; if (context->athlete->zones()) { range = context->athlete->zones()->whichRange(ride->dateTime.date()); if (range >= 0) rideCP = context->athlete->zones()->getCP(range); } loadRide(); } void GoogleMapControl::loadRide() { createHtml(); view->page()->mainFrame()->setHtml(currentPage); } void GoogleMapControl::updateFrame() { // deleting the web bridge seems to be the only way to // reset state between it and the webpage. delete webBridge; webBridge = new WebBridge(context, this); connect(context, SIGNAL(intervalsChanged()), webBridge, SLOT(intervalsChanged())); connect(context, SIGNAL(intervalSelected()), webBridge, SLOT(intervalsChanged())); 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 QColor bgColor = GColor(CPLOTBACKGROUND); QColor fgColor = GCColor::invertColor(bgColor); if(!ride || !ride->ride() || ride->ride()->areDataPresent()->lat == false || ride->ride()->areDataPresent()->lon == false) { currentPage = QString("
%3
").arg(bgColor.name()).arg(fgColor.name()).arg(tr("No GPS Data Present")); setIsBlank(true); return; } else { setIsBlank(false); } // load the Google Map v3 API currentPage = QString(" \n" "\n" "\n" " \n" "\n" "Golden Cheetah Map\n" " \n" "\n" " \n"); // fg/bg currentPage += QString("") .arg(bgColor.name()).arg(fgColor.name()); // local functions currentPage += QString("\n").arg(minLat,0,'g',GPS_COORD_TO_STRING). arg(minLon,0,'g',GPS_COORD_TO_STRING). arg(maxLat,0,'g',GPS_COORD_TO_STRING). arg(maxLon,0,'g',GPS_COORD_TO_STRING); // 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(context->athlete->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"); // Listen mouse events code += QString("google.maps.event.addListener(polyline, 'mousedown', function(event) { map.setOptions({draggable: false, zoomControl: false, scrollwheel: false, disableDoubleClickZoom: true}); webBridge.clickPath(event.latLng.lat(), event.latLng.lng()); });\n" "google.maps.event.addListener(polyline, 'mouseup', function(event) { map.setOptions({draggable: true, zoomControl: true, scrollwheel: true, disableDoubleClickZoom: false}); webBridge.mouseup(); });\n" "google.maps.event.addListener(polyline, 'mouseover', function(event) { webBridge.hoverPath(event.latLng.lat(), event.latLng.lng()); });\n"); } else { if (rfp->lat || rfp->lon) code += QString("path.push(new google.maps.LatLng(%1,%2));\n").arg(rfp->lat,0,'g',GPS_COORD_TO_STRING).arg(rfp->lon,0,'g',GPS_COORD_TO_STRING); } // 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" "}\n").arg(color.name()); view->page()->mainFrame()->evaluateJavaScript(code); } } } void GoogleMapControl::clearTempInterval() { QString code = QString( "{ \n" " tmpIntervalHighlighter.getPath().clear();\n" "}\n" ); view->page()->mainFrame()->evaluateJavaScript(code); } void GoogleMapControl::drawTempInterval(IntervalItem *current) { QString code = QString( "{ \n" // interval will be drawn with these options " var polyOptions = {\n" " strokeColor: '#00FFFF',\n" " strokeOpacity: 0.6,\n" " strokeWeight: 10,\n" " zIndex: -1\n" // put at the bottom " }\n" " if (!tmpIntervalHighlighter) {\n" " tmpIntervalHighlighter = new google.maps.Polyline(polyOptions);\n" " tmpIntervalHighlighter.setMap(map);\n" " google.maps.event.addListener(tmpIntervalHighlighter, 'mouseup', function(event) { map.setOptions({draggable: true, zoomControl: true, scrollwheel: true, disableDoubleClickZoom: false}); webBridge.mouseup(); });\n" " } \n" " var path = tmpIntervalHighlighter.getPath();\n" " path.clear();\n"); foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) { if (rfp->secs+myRideItem->ride()->recIntSecs() > current->start && rfp->secs< current->stop) { if (rfp->lat || rfp->lon) { code += QString(" path.push(new google.maps.LatLng(%1,%2));\n").arg(rfp->lat,0,'g',GPS_COORD_TO_STRING).arg(rfp->lon,0,'g',GPS_COORD_TO_STRING); } } } code += QString("}\n" ); view->page()->mainFrame()->evaluateJavaScript(code); overlayIntervals->intervalSelected(); } // // Static helper - havervine formula 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',GPS_COORD_TO_STRING).arg(points[0]->lon,0,'g',GPS_COORD_TO_STRING); 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',GPS_COORD_TO_STRING).arg(points[0]->lon,0,'g',GPS_COORD_TO_STRING); 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',GPS_COORD_TO_STRING).arg(points[points.count()-1]->lon,0,'g',GPS_COORD_TO_STRING); 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',GPS_COORD_TO_STRING).arg(rfp->lon,0,'g',GPS_COORD_TO_STRING); view->page()->mainFrame()->evaluateJavaScript(code); stoptime=0; } stoplat=stoplon=stoptime=0; } laststoptime = rfp->secs; } // // INTERVAL MARKERS // int interval=0; foreach (IntervalItem *x, myRideItem->intervals()) { int offset = myRideItem->ride()->intervalBeginSecs(x->start); 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); });" " google.maps.event.addListener(marker, 'mouseover', function(event) { webBridge.hoverInterval(%4); });" " google.maps.event.addListener(marker, 'mouseout', function(event) { webBridge.clearHover(); });" "}") .arg(myRideItem->ride()->dataPoints()[offset]->lat,0,'g',GPS_COORD_TO_STRING) .arg(myRideItem->ride()->dataPoints()[offset]->lon,0,'g',GPS_COORD_TO_STRING) .arg(x->name) .arg(interval) ; view->page()->mainFrame()->evaluateJavaScript(code); interval++; } return; } void GoogleMapControl::zoomInterval(IntervalItem *which) { RideItem *ride = myRideItem; // null ride if (!ride || !ride->ride()) return; int start = ride->ride()->timeIndex(which->start); int end = ride->ride()->timeIndex(which->stop); // out of bounds if (start < 0 || start > ride->ride()->dataPoints().count()-1 || end < 0 || end > ride->ride()->dataPoints().count()-1) return; // Get the bounding rectangle for this interval double minLat, minLon, maxLat, maxLon; minLat = minLon = 1000; maxLat = maxLon = -1000; // larger than 360 // get bounding co-ordinates for ride for(int i=start; i<= end; i++) { RideFilePoint *rfp = ride->ride()->dataPoints().at(i); 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); } } // now zoom to interval QString code = QString("{ var southwest = new google.maps.LatLng(%1, %2);\n" "var northeast = new google.maps.LatLng(%3, %4);\n" "var bounds = new google.maps.LatLngBounds(southwest, northeast);\n" "map.fitBounds(bounds);\n }") .arg(minLat,0,'g',GPS_COORD_TO_STRING) .arg(minLon,0,'g',GPS_COORD_TO_STRING) .arg(maxLat,0,'g',GPS_COORD_TO_STRING) .arg(maxLon,0,'g',GPS_COORD_TO_STRING); view->page()->mainFrame()->evaluateJavaScript(code); } // quick diag, used to debug code only void WebBridge::call(int count) { qDebug()<<"webBridge call:"<property("ride").value(); if (rideItem) return rideItem->intervalsSelected().count(); return 0; } // get a latlon array for the i'th selected interval QVariantList WebBridge::getLatLons(int i) { QVariantList latlons; RideItem *rideItem = gm->property("ride").value(); if (rideItem && i > 0 && rideItem->intervalsSelected().count() >= i) { IntervalItem *current = rideItem->intervalsSelected().at(i-1); // 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) { RideItem *rideItem = gm->property("ride").value(); if (x < 0 || rideItem->intervals().count() <= x) return; IntervalItem *current = rideItem->intervals().at(x); if (current) { current->selected = !current->selected; context->notifyIntervalItemSelectionChanged(current); } } void WebBridge::hoverInterval(int n) { RideItem *rideItem = gm->property("ride").value(); if (rideItem && rideItem->ride() && rideItem->intervals().count() > n) { context->notifyIntervalHover(rideItem->intervals().at(n)); } } void WebBridge::clearHover() { } QList WebBridge::searchPoint(double lat, double lng) { QList list; RideItem *rideItem = gm->property("ride").value(); RideFilePoint *candidat = NULL; foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) { if (p1->lat == 0 && p1->lon == 0) continue; if (((p1->lat-lat> 0 && p1->lat-lat< 0.0001) || (p1->lat-lat< 0 && p1->lat-lat> -0.0001)) && ((p1->lon-lng> 0 && p1->lon-lng< 0.0001) || (p1->lon-lng< 0 && p1->lon-lng> -0.0001))) { // Vérifie distance avec dernier candidat candidat = p1; } else if (candidat) { list.append(candidat); candidat = NULL; } } return list; } void WebBridge::hoverPath(double lat, double lng) { if (point) { RideItem *rideItem = gm->property("ride").value(); QString name = QString(tr("Selection #%1 ")).arg(selection); if (rideItem->intervals(RideFileInterval::USER).count()) { IntervalItem *last = rideItem->intervals(RideFileInterval::USER).last(); if (last->name.startsWith(name) && last->rideInterval) { QList list = searchPoint(lat, lng); if (list.count() > 0) { RideFilePoint* secondPoint = list.at(0); if (secondPoint->secs>point->secs) { last->rideInterval->start = last->start = point->secs; last->rideInterval->stop = last->stop = secondPoint->secs; } else { last->rideInterval->stop = last->stop = point->secs; last->rideInterval->start = last->start = secondPoint->secs; } last->startKM = last->rideItem()->ride()->timeToDistance(last->start); last->stopKM = last->rideItem()->ride()->timeToDistance(last->stop); // update metrics last->refresh(); // mark dirty last->rideItem()->setDirty(true); // overlay a shaded route gm->drawTempInterval(last); // update charts etc context->notifyIntervalsChanged(); } } } // add average power to the end of the selection name //name += QString("(%1 watts)").arg(round((wattsTotal && arrayLength) ? wattsTotal/arrayLength : 0)); // now update the RideFileIntervals and all the plots etc //context->athlete->updateRideFileIntervals(); } } void WebBridge::clickPath(double lat, double lng) { selection++; RideItem *rideItem = gm->property("ride").value(); QString name = QString(tr("Selection #%1 ")).arg(selection); QList list = searchPoint(lat, lng); if (list.count() > 0) { point = list.at(0); IntervalItem *add = rideItem->newInterval(name, point->secs, point->secs, 0, 0); add->selected = true; // rebuild list in sidebar context->notifyIntervalsUpdate(rideItem); } else { point = NULL; } } void WebBridge::mouseup() { // clear the temorary highlighter if (point) { gm->clearTempInterval(); point = NULL; } } bool GoogleMapControl::event(QEvent *event) { // nasty nasty nasty hack to move widgets as soon as the widget geometry // is set properly by the layout system, by default the width is 100 and // we wait for it to be set properly then put our helper widget on the RHS if (event->type() == QEvent::Resize && geometry().width() != 100) { // put somewhere nice on first show if (firstShow) { firstShow = false; helperWidget()->move(mainWidget()->geometry().width()-275, 50); } // if off the screen move on screen if (helperWidget()->geometry().x() > geometry().width()) { helperWidget()->move(mainWidget()->geometry().width()-275, 50); } } return QWidget::event(event); }