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:
Joachim Kohlhammer
2023-11-01 15:15:56 +01:00
committed by GitHub
parent 72de6bbee1
commit 43368addf7
4 changed files with 282 additions and 11 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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