Files
GoldenCheetah/src/ScatterPlot.cpp
Mark Liversedge da8eec1b13 Further UI fixups
More work to unify the tab view with the overall
UI look-and-feel, allowing you to remove and add
tabs via mouse over rather than the close tab
icon. Also added a number of cosmetic adjustments.

Fixed up PerformanceManager and ScatterPlot to
pick up on the chart fonts and tick size etc.

Removed some erroneous static members in the
ScatterPlot that caused a SEGV when adding multiple
charts (or cancelling an add then adding again).
2011-06-10 23:27:51 +01:00

412 lines
14 KiB
C++

/*
* Copyright (c) 2009 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 "ScatterPlot.h"
#include "ScatterWindow.h"
#include "MainWindow.h"
#include "Settings.h"
#include "Zones.h"
#include "Colors.h"
#include "RideFile.h"
#include "Units.h" // for MILES_PER_KM
#include <QWidget>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_plot_canvas.h>
#include <qwt_scale_draw.h>
#include <qwt_symbol.h>
#define PI M_PI
static double
pointType(const RideFilePoint *point, int type, bool useMetricUnits, double cranklength)
{
// return the point value for the given type
switch(type) {
case MODEL_POWER : return point->watts;
case MODEL_CADENCE : return point->cad;
case MODEL_HEARTRATE : return point->hr;
case MODEL_SPEED :
if (useMetricUnits == true){
return point->kph;
}else {
return point->kph * MILES_PER_KM;
}
case MODEL_ALT :
if (useMetricUnits == true){
return point->alt;
}else {
return point->alt * FEET_PER_METER;
}
case MODEL_TORQUE : return point->nm;
case MODEL_TIME : return point->secs;
case MODEL_DISTANCE :
if (useMetricUnits == true){
return point->km;
}else {
return point->km * MILES_PER_KM;
}
case MODEL_INTERVAL : return point->interval;
case MODEL_LAT : return point->lat;
case MODEL_LONG : return point->lon;
case MODEL_AEPF :
if (point->watts == 0 || point->cad == 0) return 0;
else return ((point->watts * 60.0) / (point->cad * cranklength * 2.0 * PI));
case MODEL_CPV :
return ((point->cad * cranklength * 2.0 * PI) / 60.0);
// these you need to do yourself cause there is some
// logic needed and I'm just lookup table!
case MODEL_XYTIME : return 1;
case MODEL_POWERZONE : return point->watts;
}
return 0; // ? unknown channel ?
}
static QString describeType(int type, bool longer, bool useMetricUnits)
{
// return the point value for the given type
if (longer == true) {
switch(type) {
case MODEL_POWER : return ("Power (watts)");
case MODEL_CADENCE : return ("Cadence (rpm)");
case MODEL_HEARTRATE : return ("Heartrate (bpm)");
case MODEL_SPEED :
if (useMetricUnits == true){
return ("Speed (kph)");
}else {
return ("Speed (mph)");
}
case MODEL_ALT :
if (useMetricUnits == true){
return ("Altitude (meters)");
}else {
return ("Altitude (feet)");
}
case MODEL_TORQUE : return ("Torque (N)");
case MODEL_TIME : return ("Elapsed Time (secs)");
case MODEL_DISTANCE :
if (useMetricUnits == true){
return ("Elapsed Distance (km)");
}else {
return ("Elapsed Distance (mi)");
}
case MODEL_INTERVAL : return ("Interval Number"); // XXX implemented differently
case MODEL_LAT : return ("Latitude (degree offset)");
case MODEL_LONG : return ("Longitude (degree offset)");
case MODEL_CPV : return ("Circumferential Pedal Velocity (cm/s)");
case MODEL_AEPF : return ("Average Effective Pedal Force (N)");
// these you need to do yourself cause there is some
// logic needed and I'm just lookup table!
case MODEL_XYTIME : return ("Time at X/Y (%)");
case MODEL_POWERZONE : return ("Power Zone");
}
return ("Unknown");; // ? unknown channel ?
} else {
switch(type) {
case MODEL_POWER : return ("Power");
case MODEL_CADENCE : return ("Cadence");
case MODEL_HEARTRATE : return ("Heartrate");
case MODEL_SPEED : return ("Speed"); //XXX metric / imperial!
case MODEL_ALT : return ("Altitude"); // XXX metric / imperial
case MODEL_TORQUE : return ("Pedal Force");
case MODEL_TIME : return ("Time");
case MODEL_DISTANCE : return ("Distance");//XXX metric/imperial
case MODEL_INTERVAL : return ("Interval"); // XXX implemented differently
case MODEL_LAT : return ("Latitude");
case MODEL_LONG : return ("Longitude");
case MODEL_XYTIME : return ("Time at X/Y");
case MODEL_POWERZONE : return ("Zone");
case MODEL_CPV : return ("CPV");
case MODEL_AEPF : return ("AEPF");
}
return ("None");; // ? unknown channel ?
}
}
ScatterPlot::ScatterPlot(MainWindow *parent) : main(parent)
{
setInstanceName("2D Plot");
all = NULL;
grid = NULL;
canvas()->setFrameStyle(QFrame::NoFrame);
setAxisMaxMinor(xBottom, 0);
setAxisMaxMinor(yLeft, 0);
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
setAxisScaleDraw(QwtPlot::xBottom, sd);
sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
setAxisScaleDraw(QwtPlot::yLeft, sd);
connect(main, SIGNAL(configChanged()), this, SLOT(configChanged()));
configChanged(); // use latest colors etc
}
void ScatterPlot::setData (ScatterSettings *settings)
{
// get application settings
cranklength = appsettings->value(this, GC_CRANKLENGTH, 0.0).toDouble() / 1000.0;
useMetricUnits = appsettings->value(this, GC_UNIT).toString() == "Metric";
// if there are no settings or incomplete settings
// create a null data plot
if (settings == NULL || settings->ride == NULL || settings->ride->ride() == NULL ||
settings->x == 0 || settings->y == 0 ) {
return;
}
// if its not setup or no settings exist default to 175mm cranks
if (cranklength == 0.0) cranklength = 0.175;
//
// Create Main Plot dataset - used to frame intervals
//
int points=0;
x.clear();
y.clear();
x.resize(settings->ride->ride()->dataPoints().count());
y.resize(settings->ride->ride()->dataPoints().count());
double maxY = maxX = -65535;
double minY = minX = 65535;
foreach(const RideFilePoint *point, settings->ride->ride()->dataPoints()) {
double xv = x[points] = pointType(point, settings->x, useMetricUnits, cranklength);
double yv = y[points] = pointType(point, settings->y, useMetricUnits, cranklength);
// skip zeroes?
if (!(settings->ignore && (x[points] == 0 || y[points] == 0))) {
points++;
if (yv > maxY) maxY = yv;
if (yv < minY) minY = yv;
if (xv > maxX) maxX = xv;
if (xv < minX) minX = xv;
}
}
QwtSymbol sym;
sym.setStyle(QwtSymbol::Ellipse);
sym.setSize(6);
sym.setPen(GCColor::invert(GColor(CPLOTBACKGROUND)));
sym.setBrush(QBrush(Qt::NoBrush));
// wipe away existing
if (all) {
all->detach();
delete all;
}
// setup the framing curve
if (settings->frame) {
all = new QwtPlotCurve();
all->setSymbol(sym);
all->setStyle(QwtPlotCurve::Dots);
all->setRenderHint(QwtPlotItem::RenderAntialiased);
all->setData(x.constData(), y.constData(), points);
all->attach(this);
} else {
all = NULL;
}
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
if (grid) {
grid->detach();
delete grid;
}
if (settings->gridlines) {
grid = new QwtPlotGrid();
grid->setPen(gridPen);
grid->enableX(true);
grid->enableY(true);
grid->attach(this);
} else {
grid = NULL;
}
setAxisTitle(yLeft, describeType(settings->y, true, useMetricUnits));
setAxisTitle(xBottom, describeType(settings->x, true, useMetricUnits));
// truncate PfPv values to make easier to read
if (settings->y == MODEL_AEPF) setAxisScale(yLeft, 0, 600);
else setAxisScale(yLeft, minY, maxY);
if (settings->x == MODEL_CPV) setAxisScale(xBottom, 0, 3);
else setAxisScale(xBottom, minX, maxX);
//
// Create Interval Plot dataset - used to frame intervals
//
// clear out any interval curves which are presently defined
if (intervalCurves.size()) {
QListIterator<QwtPlotCurve *> i(intervalCurves);
while (i.hasNext()) {
QwtPlotCurve *curve = i.next();
curve->detach();
delete curve;
}
}
intervalCurves.clear();
// which ones are highlighted then?
QVector<int> intervals;
QMap<int,int> displaySequence;
for (int child=0; child<main->allIntervalItems()->childCount(); child++) {
IntervalItem *current = dynamic_cast<IntervalItem *>(main->allIntervalItems()->child(child));
if ((current != NULL) && current->isSelected()) {
intervals.append(child);
displaySequence.insert(current->displaySequence, intervals.count()-1);
}
}
if (intervals.count() > 0) {
// interval data in here
QVector<QVector<double> > xvals(intervals.count()); // array of curve x arrays
QVector<QVector<double> > yvals(intervals.count()); // array of curve x arrays
QVector<int> points(intervals.count()); // points in eac curve
// extract interval data
foreach(const RideFilePoint *point, settings->ride->ride()->dataPoints()) {
double x = pointType(point, settings->x, useMetricUnits, cranklength);
double y = pointType(point, settings->y, useMetricUnits, cranklength);
if (!(settings->ignore && (x == 0 && y ==0))) {
// which interval is it in?
for (int idx=0; idx<intervals.count(); idx++) {
IntervalItem *current = dynamic_cast<IntervalItem *>(main->allIntervalItems()->child(intervals[idx]));
if (point->secs+settings->ride->ride()->recIntSecs() > current->start && point->secs< current->stop) {
xvals[idx].append(x);
yvals[idx].append(y);
points[idx]++;
}
}
}
}
// now we have the interval data lets create the curves
QMapIterator<int, int> order(displaySequence);
while (order.hasNext()) {
order.next();
int idx = order.value();
QPen pen;
QColor intervalColor;
intervalColor.setHsv((255/main->allIntervalItems()->childCount()) * (intervals[idx]), 255,255);
pen.setColor(intervalColor);
sym.setPen(pen);
QwtPlotCurve *curve = new QwtPlotCurve();
curve->setSymbol(sym);
curve->setStyle(QwtPlotCurve::Dots);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curve->setData(xvals[idx].constData(), yvals[idx].constData(), points[idx]);
curve->attach(this);
intervalCurves.append(curve);
}
}
replot();
}
void
ScatterPlot::configChanged()
{
// setColors bg
setCanvasBackground(GColor(CPLOTBACKGROUND));
QColor grid = GColor(CPLOTGRID);
}
void
ScatterPlot::showTime(ScatterSettings *settings, int offset, int secs)
{
static QwtPlotCurve *time = NULL;
int begin = offset-secs;
if (begin<0) begin = 0;
if (time == NULL) {
time = new QwtPlotCurve();
time->attach(this);
replot();
}
// offset into data points...
if (settings->ride->ride()) {
int startidx = settings->ride->ride()->timeIndex(begin);
int stopidx = settings->ride->ride()->timeIndex(offset);
QwtSymbol sym;
sym.setStyle(QwtSymbol::Rect);
sym.setSize(6);
sym.setPen(GCColor::invert(GColor(CPLOTBACKGROUND)));
sym.setBrush(QBrush(Qt::NoBrush));
QPen pen;
pen.setColor(Qt::red);
sym.setPen(pen);
time->setSymbol(sym);
time->setStyle(QwtPlotCurve::Dots);
time->setRenderHint(QwtPlotItem::RenderAntialiased);
time->setData(x.constData()+startidx, y.constData()+startidx, stopidx-startidx);
// make it on top
time->detach();
time->attach(this);
replot(); // ouch really expensive if lots on the plot...
}
}
void
ScatterPlot::setAxisTitle(int axis, QString label)
{
// setup the default fonts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
QwtText title(label);
title.setFont(stGiles);
QwtPlot::setAxisFont(axis, stGiles);
QwtPlot::setAxisTitle(axis, title);
}