mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
Mapview: Added interactivity to the Smallplot (#4408)
* Added a tooltip showing data about the current position (Altitude, Power, HR, Time) * Added a marker to the map corresponding to the current mouse position (both Google and OSM) * Changed scaling of the smallplot: Chart is now between lowest point and highest point instead of sea level and highest point
This commit is contained in:
committed by
GitHub
parent
72de6bbee1
commit
43368addf7
@@ -174,6 +174,9 @@ RideMapWindow::RideMapWindow(Context *context, int mapType) : GcChartWindow(cont
|
||||
|
||||
// just the hr and power as a plot
|
||||
smallPlot = new SmallPlot(this);
|
||||
smallPlot->enableTracking();
|
||||
connect(smallPlot, SIGNAL(selectedPosX(double)), this, SLOT(showPosition(double)));
|
||||
connect(smallPlot, SIGNAL(mouseLeft()), this, SLOT(hidePosition()));
|
||||
smallPlot->setMaximumHeight(200);
|
||||
smallPlot->setMinimumHeight(100);
|
||||
smallPlot->setVisible(false);
|
||||
@@ -201,6 +204,26 @@ RideMapWindow::mapTypeSelected(int x)
|
||||
forceReplot();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RideMapWindow::showPosition(double mins)
|
||||
{
|
||||
long secs = mins * 60;
|
||||
int idx = secs / 5;
|
||||
idx = std::max(idx, 0);
|
||||
idx = std::min(idx, positionItems.length() - 1);
|
||||
PositionItem positionItem = positionItems.at(idx);
|
||||
view->page()->runJavaScript(QString("setPosMarker(%1, %2);").arg(positionItem.lat).arg(positionItem.lng));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RideMapWindow::hidePosition()
|
||||
{
|
||||
view->page()->runJavaScript(QString("hidePosMarker();"));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RideMapWindow::setCustomTSWidgetVisible(bool value)
|
||||
{
|
||||
@@ -411,6 +434,7 @@ RideMapWindow::rideSelected()
|
||||
void RideMapWindow::loadRide()
|
||||
{
|
||||
createHtml();
|
||||
buildPositionList();
|
||||
|
||||
view->page()->setHtml(currentPage);
|
||||
}
|
||||
@@ -519,7 +543,27 @@ void RideMapWindow::createHtml()
|
||||
currentPage += QString("var intervalList;\n" // array of intervals
|
||||
"var markerList;\n" // array of markers
|
||||
"var polyList;\n" // array of polylines
|
||||
"var tmpIntervalHighlighter;\n"); // temp interval
|
||||
"var tmpIntervalHighlighter;\n" // temp interval
|
||||
"var posMarker;\n"); // marker for position tracking
|
||||
}
|
||||
|
||||
if (mapCombo->currentIndex() == OSM) {
|
||||
currentPage += QString("const svgPosIcon = L.divIcon({\n"
|
||||
"html: `\n"
|
||||
"<svg\n"
|
||||
" width=\"16\"\n"
|
||||
" height=\"16\"\n"
|
||||
" viewBox=\"0 0 100 100\"\n"
|
||||
" version=\"1.1\"\n"
|
||||
" preserveAspectRatio=\"none\"\n"
|
||||
" xmlns=\"http://www.w3.org/2000/svg\"\n"
|
||||
">\n"
|
||||
" <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"green\"/>\n"
|
||||
"</svg>`,\n"
|
||||
" className: \"svg-pos-marker\",\n"
|
||||
" iconSize: [16, 16],\n"
|
||||
" iconAnchor: [8, 8],\n"
|
||||
"});\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -747,6 +791,46 @@ void RideMapWindow::createHtml()
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Position Marker
|
||||
|
||||
if (mapCombo->currentIndex() == OSM) {
|
||||
currentPage += QString("function setPosMarker(lat, lng) {\n"
|
||||
" var latlng = new L.LatLng(lat, lng);\n"
|
||||
" if (typeof posMarker !== 'undefined') {\n"
|
||||
" posMarker.setLatLng(latlng);\n"
|
||||
" } else {\n"
|
||||
" posMarker = new L.marker(latlng, {icon: svgPosIcon});\n"
|
||||
" posMarker.addTo(map);\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function hidePosMarker() {\n"
|
||||
" if (typeof posMarker !== 'undefined') {\n"
|
||||
" posMarker.remove();\n"
|
||||
" posMarker = undefined;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
} else if (mapCombo->currentIndex() == GOOGLE) {
|
||||
currentPage += QString("function setPosMarker(lat, lng) {\n"
|
||||
" var latlng = new google.maps.LatLng(lat, lng);\n"
|
||||
" if (typeof posMarker !== 'undefined') {\n"
|
||||
" posMarker.setPosition(latlng);\n"
|
||||
" } else {\n"
|
||||
" posMarker = new google.maps.Marker({ position: latlng });\n"
|
||||
" posMarker.setMap(map);\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"function hidePosMarker() {\n"
|
||||
" if (typeof posMarker !== 'undefined') {\n"
|
||||
" posMarker.setMap(null);\n"
|
||||
" posMarker = undefined;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Initialize
|
||||
|
||||
@@ -928,6 +1012,37 @@ RideMapWindow::getCompareBoundingBox
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RideMapWindow::buildPositionList
|
||||
()
|
||||
{
|
||||
double lastLat = 1000;
|
||||
double lastLon = 1000;
|
||||
long lastSecs = -5;
|
||||
bool first = true;
|
||||
positionItems.clear();
|
||||
foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) {
|
||||
long secs = rfp->secs;
|
||||
if (first) {
|
||||
secs = 0;
|
||||
first = false;
|
||||
}
|
||||
if (secs % 5 == 0 && secs - lastSecs == 5) {
|
||||
lastLat = rfp->lat;
|
||||
lastLon = rfp->lon;
|
||||
positionItems.append(PositionItem(lastLat, lastLon));
|
||||
lastSecs = secs;
|
||||
} else if (secs - lastSecs > 5) {
|
||||
// Add dummy points with last known position if not moving
|
||||
while (lastSecs < secs) {
|
||||
lastSecs += 5;
|
||||
positionItems.append(PositionItem(lastLat, lastLon));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QColor RideMapWindow::GetColor(int watts)
|
||||
{
|
||||
if (range < 0 || hideShadedZones()) return GColor(MAPROUTELINE);
|
||||
|
||||
@@ -48,6 +48,14 @@ class RideMapWindow;
|
||||
class IntervalSummaryWindow;
|
||||
class SmallPlot;
|
||||
|
||||
|
||||
struct PositionItem {
|
||||
PositionItem(double lat, double lng): lat(lat), lng(lng) {}
|
||||
|
||||
double lat, lng;
|
||||
};
|
||||
|
||||
|
||||
// trick the maps api into ignoring gestures by
|
||||
// pretending to be chrome. see: http://developer.qt.nokia.com/forums/viewthread/1643/P15
|
||||
class mapWebPage : public QWebEnginePage
|
||||
@@ -185,6 +193,9 @@ class RideMapWindow : public GcChartWindow
|
||||
void drawTempInterval(IntervalItem *current);
|
||||
void clearTempInterval();
|
||||
|
||||
void showPosition(double mins);
|
||||
void hidePosition();
|
||||
|
||||
void compareIntervalsStateChanged(bool state);
|
||||
void compareIntervalsChanged();
|
||||
|
||||
@@ -215,10 +226,13 @@ class RideMapWindow : public GcChartWindow
|
||||
bool firstShow;
|
||||
IntervalSummaryWindow *overlayIntervals;
|
||||
|
||||
QList<PositionItem> positionItems;
|
||||
|
||||
QString osmTileServerUrlDefault;
|
||||
|
||||
QColor GetColor(int watts);
|
||||
void createHtml();
|
||||
void buildPositionList();
|
||||
|
||||
bool getCompareBoundingBox(double &minLat, double &maxLat, double &minLon, double &maxLon) const;
|
||||
|
||||
|
||||
@@ -27,14 +27,83 @@
|
||||
#include <qwt_plot_canvas.h>
|
||||
#include <qwt_plot_grid.h>
|
||||
#include <qwt_plot_marker.h>
|
||||
#include <qwt_plot_picker.h>
|
||||
#include <qwt_picker_machine.h>
|
||||
#include <qwt_text.h>
|
||||
#include <qwt_legend.h>
|
||||
#include <qwt_series_data.h>
|
||||
#include <qwt_compat.h>
|
||||
|
||||
static double inline max(double a, double b) { if (a > b) return a; else return b; }
|
||||
|
||||
SmallPlot::SmallPlot(QWidget *parent) : QwtPlot(parent), d_mrk(NULL), smooth(30)
|
||||
SmallPlotPicker::SmallPlotPicker(QWidget *canvas) : QwtPlotPicker(canvas)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
QwtText
|
||||
SmallPlotPicker::trackerText(const QPoint &point) const
|
||||
{
|
||||
QPointF pointF = invTransform(point);
|
||||
if (pointF.x() < 0) {
|
||||
return QwtText("");
|
||||
}
|
||||
int intNormX = pointF.x();
|
||||
int hours = intNormX / 60;
|
||||
int mins = intNormX % 60;
|
||||
int secs = (pointF.x() - intNormX) * 60;
|
||||
bool firstLine = true;
|
||||
|
||||
QString text;
|
||||
QwtPlotItemList plotItems = plot()->itemList(QwtPlotItem::Rtti_PlotCurve);
|
||||
foreach (QwtPlotItem *plotItem, plotItems) {
|
||||
QwtPlotCurve *plotCurve = static_cast<QwtPlotCurve *>(plotItem);
|
||||
int idx = 0;
|
||||
size_t size = plotCurve->data()->size();
|
||||
double dist = 100000;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
QPointF nextPoint = plotCurve->data()->sample(i);
|
||||
double newDist = fabs(nextPoint.x() - pointF.x());
|
||||
if (newDist <= dist) {
|
||||
idx = i;
|
||||
dist = newDist;
|
||||
} else {
|
||||
QPointF thisPoint = plotCurve->data()->sample(idx);
|
||||
if (thisPoint.y() > 0) {
|
||||
if (! firstLine) {
|
||||
text.append("\n");
|
||||
}
|
||||
text.append(QString("%1: %2").arg(plotCurve->title().text())
|
||||
.arg(int(thisPoint.y())));
|
||||
firstLine = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! firstLine) {
|
||||
text.append("\n");
|
||||
}
|
||||
text.append(QString("%1:%2:%3").arg(hours)
|
||||
.arg(mins, 2, 10, QChar('0'))
|
||||
.arg(secs, 2, 10, QChar('0')));
|
||||
|
||||
QwtText tooltip(text);
|
||||
QFont stGiles;
|
||||
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
|
||||
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
|
||||
stGiles.setWeight(QFont::Bold);
|
||||
tooltip.setFont(stGiles);
|
||||
|
||||
tooltip.setBackgroundBrush(QBrush(GColor(CPLOTMARKER)));
|
||||
tooltip.setColor(GColor(CRIDEPLOTBACKGROUND));
|
||||
tooltip.setBorderRadius(6);
|
||||
tooltip.setRenderFlags(Qt::AlignCenter | Qt::AlignVCenter);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
|
||||
SmallPlot::SmallPlot(QWidget *parent) : QwtPlot(parent), d_mrk(NULL), smooth(30), tracking(false)
|
||||
{
|
||||
setCanvasBackground(GColor(CPLOTBACKGROUND));
|
||||
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
|
||||
@@ -97,6 +166,18 @@ struct DataPoint {
|
||||
time(t), hr(h), alt(a), watts(w), inter(i) {}
|
||||
};
|
||||
|
||||
void
|
||||
SmallPlot::enableTracking()
|
||||
{
|
||||
tracking = true;
|
||||
QwtPlotPicker *picker = new SmallPlotPicker(canvas());
|
||||
picker->setTrackerMode(QwtPlotPicker::ActiveOnly);
|
||||
picker->setStateMachine(new QwtPickerTrackerMachine());
|
||||
picker->setRubberBand(QwtPicker::VLineRubberBand);
|
||||
picker->setRubberBandPen(QPen(GColor(CPLOTMARKER)));
|
||||
connect(picker, SIGNAL(moved(const QPoint&)), this, SLOT(pointMoved(const QPoint&)));
|
||||
}
|
||||
|
||||
void
|
||||
SmallPlot::recalc()
|
||||
{
|
||||
@@ -183,23 +264,35 @@ SmallPlot::setYMax()
|
||||
{
|
||||
double ymax = 0;
|
||||
double y1max = 500;
|
||||
double y1min = 500;
|
||||
QString ylabel = "";
|
||||
QString y1label = "";
|
||||
if (wattsCurve->isVisible()) {
|
||||
ymax = max(ymax, wattsCurve->maxYValue());
|
||||
ymax = std::max(ymax, wattsCurve->maxYValue());
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + tr("Watts");
|
||||
}
|
||||
if (hrCurve->isVisible()) {
|
||||
ymax = max(ymax, hrCurve->maxYValue());
|
||||
ymax = std::max(ymax, hrCurve->maxYValue());
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + tr("BPM");
|
||||
}
|
||||
if (altCurve->isVisible()) {
|
||||
y1max = max(y1max, altCurve->maxYValue());
|
||||
y1label = "m";
|
||||
size_t size = altCurve->data()->size();
|
||||
double curMinY = 10000;
|
||||
double curMaxY = 0;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
double nextY = altCurve->data()->sample(i).y();
|
||||
if (nextY > 0.0) {
|
||||
curMinY = std::min(curMinY, nextY);
|
||||
curMaxY = std::max(curMaxY, nextY);
|
||||
}
|
||||
}
|
||||
y1min = curMinY;
|
||||
y1max = curMaxY;
|
||||
y1label = "m";
|
||||
}
|
||||
setAxisScale(QwtAxisId(QwtAxis::yLeft,0), 0.0, ymax * 1.1);
|
||||
setAxisTitle(QwtAxisId(QwtAxis::yLeft,0), ylabel);
|
||||
setAxisScale(QwtAxisId(QwtAxis::yLeft,1), 0.0, y1max * 1.1);
|
||||
setAxisScale(QwtAxisId(QwtAxis::yLeft,1), y1min * 0.9, y1max * 1.1);
|
||||
setAxisTitle(QwtAxisId(QwtAxis::yLeft,1), y1label);
|
||||
setAxisVisible(QwtAxisId(QwtAxis::yLeft,0), false); // hide for a small plot
|
||||
setAxisVisible(QwtAxisId(QwtAxis::yLeft,1), false); // hide for a small plot
|
||||
@@ -226,6 +319,13 @@ SmallPlot::setAxisTitle(QwtAxisId axis, QString label)
|
||||
QwtPlot::setAxisTitle(axis, title);
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
SmallPlot::hasTracking() const
|
||||
{
|
||||
return tracking;
|
||||
}
|
||||
|
||||
void
|
||||
SmallPlot::setData(RideItem *rideItem)
|
||||
{
|
||||
@@ -246,9 +346,9 @@ SmallPlot::setData(RideFile *ride)
|
||||
arrayLength = 0;
|
||||
foreach (const RideFilePoint *point, ride->dataPoints()) {
|
||||
timeArray[arrayLength] = point->secs;
|
||||
wattsArray[arrayLength] = max(0, point->watts);
|
||||
hrArray[arrayLength] = max(0, point->hr);
|
||||
altArray[arrayLength] = max(0, point->alt);
|
||||
wattsArray[arrayLength] = std::max(0., point->watts);
|
||||
hrArray[arrayLength] = std::max(0., point->hr);
|
||||
altArray[arrayLength] = std::max(0., point->alt);
|
||||
interArray[arrayLength] = point->interval;
|
||||
++arrayLength;
|
||||
}
|
||||
@@ -277,3 +377,18 @@ SmallPlot::setSmoothing(int value)
|
||||
smooth = value;
|
||||
recalc();
|
||||
}
|
||||
|
||||
void
|
||||
SmallPlot::pointMoved(const QPoint &pos)
|
||||
{
|
||||
double dataPosX = invTransform(QwtAxis::xBottom, pos.x());
|
||||
emit selectedPosX(dataPosX);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SmallPlot::leaveEvent(QEvent *event)
|
||||
{
|
||||
emit mouseLeft();
|
||||
QWidget::leaveEvent(event);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define _GC_SmallPlot_h 1
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <qwt_plot_picker.h>
|
||||
#include <QtGui>
|
||||
|
||||
class QwtPlotCurve;
|
||||
@@ -37,6 +38,8 @@ class SmallPlot : public QwtPlot
|
||||
SmallPlot(QWidget *parent=0);
|
||||
|
||||
|
||||
void enableTracking();
|
||||
bool hasTracking() const;
|
||||
int smoothing() const { return smooth; }
|
||||
void setData(RideItem *rideItem);
|
||||
void setData(RideFile *rideFile);
|
||||
@@ -51,8 +54,15 @@ class SmallPlot : public QwtPlot
|
||||
void showHr(int state);
|
||||
void setSmoothing(int value);
|
||||
|
||||
signals:
|
||||
|
||||
void selectedPosX(double dataPosX);
|
||||
void mouseLeft();
|
||||
|
||||
protected:
|
||||
|
||||
virtual void leaveEvent(QEvent *event);
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
QwtPlotCurve *wattsCurve;
|
||||
QwtPlotCurve *hrCurve;
|
||||
@@ -69,6 +79,23 @@ class SmallPlot : public QwtPlot
|
||||
QVector<int> interArray;
|
||||
|
||||
int smooth;
|
||||
bool tracking;
|
||||
|
||||
protected slots:
|
||||
|
||||
void pointMoved(const QPoint &pos);
|
||||
};
|
||||
|
||||
|
||||
class SmallPlotPicker : public QwtPlotPicker
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SmallPlotPicker(QWidget *canvas);
|
||||
|
||||
protected:
|
||||
virtual QwtText trackerText(const QPoint &point) const override;
|
||||
};
|
||||
|
||||
#endif // _GC_SmallPlot_h
|
||||
|
||||
Reference in New Issue
Block a user