Files
GoldenCheetah/src/LTMPlot.cpp
Mark Liversedge c4f82e19b6 Better Zones Configuration Page
The zone ranges configuration page caused a SEGV when deleting the
last zone. On inspection the zone configuration needed to be
revised since the UI was confusing and didn't allow fine grained
user editing (relying upon manual editing of the power.zones file).

The UI has been redesigned and fine grained editing of ranges, zones
and default zones is now supported.

The Zones class has been slightly modified to support the new UI and
existing members are better commented. In addition, the read/write
functions have been updated to always include the DEFAULTS section and
to set defaults according to manual zone setups when it is not present
(legacy support).

There are now 10 TimeInZone metrics to match the maximum of 10 zones
the user can define.

Fixes #78.

Fixes #34.
2010-06-06 18:36:50 -07:00

928 lines
32 KiB
C++

/*
* Copyright (c) 2010 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 "LTMPlot.h"
#include "LTMTool.h"
#include "LTMTrend.h"
#include "LTMWindow.h"
#include "MetricAggregator.h"
#include "SummaryMetrics.h"
#include "RideMetric.h"
#include "Settings.h"
#include "Colors.h"
#include "StressCalculator.h" // for LTS/STS calculation
#include <QSettings>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_curve_fitter.h>
#include <qwt_plot_grid.h>
#include <qwt_symbol.h>
#include <math.h> // for isinf() isnan()
#include <boost/shared_ptr.hpp>
static int supported_axes[] = { QwtPlot::yLeft, QwtPlot::yRight, QwtPlot::yLeft1, QwtPlot::yRight1, QwtPlot::yLeft2, QwtPlot::yRight2, QwtPlot::yLeft3, QwtPlot::yRight3 };
LTMPlot::LTMPlot(LTMWindow *parent, MainWindow *main, QDir home) : bg(NULL), parent(parent), main(main),
home(home), highlighter(NULL)
{
// get application settings
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setAxisTitle(yLeft, tr(""));
setAxisTitle(xBottom, "Date");
setAxisMaxMinor(QwtPlot::xBottom,-1);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(QDateTime::currentDateTime(), 0, LTM_DAY));
grid = new QwtPlotGrid();
grid->enableX(false);
grid->attach(this);
settings = NULL;
configUpdate(); // set basic colors
connect(main, SIGNAL(configChanged()), this, SLOT(configUpdate()));
}
LTMPlot::~LTMPlot()
{
}
void
LTMPlot::configUpdate()
{
// get application settings
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
useMetricUnits = appsettings->value(GC_UNIT).toString() == "Metric";
// set basic plot colors
setCanvasBackground(GColor(CPLOTBACKGROUND));
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
}
void
LTMPlot::setData(LTMSettings *set)
{
settings = set;
// crop dates to at least within a year of the data available, but only if we have some data
if (settings->data != NULL && (*settings->data).count() != 0) {
// if dates are null we need to set them from the available data
// end
if (settings->end == QDateTime() ||
settings->end > (*settings->data).last().getRideDate().addDays(365)) {
if (settings->end < QDateTime::currentDateTime()) {
settings->end = QDateTime::currentDateTime();
} else {
settings->end = (*settings->data).last().getRideDate();
}
}
// start
if (settings->start == QDateTime() ||
settings->start < (*settings->data).first().getRideDate().addDays(-365)) {
settings->start = (*settings->data).first().getRideDate();
}
}
setTitle(settings->title);
// wipe existing curves/axes details
QHashIterator<QString, QwtPlotCurve*> c(curves);
while (c.hasNext()) {
c.next();
QString symbol = c.key();
QwtPlotCurve *current = c.value();
//current->detach(); // the destructor does this for you
delete current;
}
curves.clear();
if (highlighter) {
highlighter->detach();
delete highlighter;
highlighter = NULL;
}
// disable all y axes until we have populated
for (int i=0; i<8; i++) enableAxis(supported_axes[i], false);
axes.clear();
// reset all min/max Y values
for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0;
// no data to display so that all folks
if (settings->data == NULL || (*settings->data).count() == 0) {
// tidy up the bottom axis
maxX = groupForDate(settings->end.date(), settings->groupBy) -
groupForDate(settings->start.date(), settings->groupBy);
setAxisScale(xBottom, 0, maxX);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
// remove the shading if it exists
refreshZoneLabels(-1);
replot();
return;
}
// setup the curves
int count;
foreach (MetricDetail metricDetail, settings->metrics) {
QVector<double> xdata, ydata;
createCurveData(settings, metricDetail, xdata, ydata, count);
// Create a curve
QwtPlotCurve *current = new QwtPlotCurve(metricDetail.uname);
curves.insert(metricDetail.symbol, current);
current->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen cpen = QPen(metricDetail.penColor);
cpen.setWidth(1.0);
current->setPen(cpen);
current->setStyle(metricDetail.curveStyle);
QwtSymbol sym;
sym.setStyle(metricDetail.symbolStyle);
if (metricDetail.curveStyle == QwtPlotCurve::Steps) {
// fill the bars
QColor brushColor = metricDetail.penColor;
brushColor.setAlpha(100);
QBrush brush = QBrush(brushColor);
current->setBrush(brush);
current->setPen(cpen);
current->setCurveAttribute(QwtPlotCurve::Inverted, true);
// XXX Symbol for steps looks horrible
sym.setStyle(QwtSymbol::Ellipse);
if (settings->groupBy == LTM_DAY)
sym.setSize(3);
else
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
// XXX FUDGE QWT's LACK OF A BAR CHART
// add a zero point at the head and tail so the
// histogram columns look nice.
// and shift all the x-values left by 0.5 so that
// they centre over x-axis labels
int i=0;
for (i=0; i<=count; i++) xdata[i] -= 0.5;
// now add a final 0 value to get the last
// column drawn - no resize neccessary
// since it is always sized for 1 + maxnumber of entries
xdata[i] = xdata[i-1] + 1;
ydata[i] = 0;
count++;
// END OF FUDGE
} else if (metricDetail.curveStyle == QwtPlotCurve::Lines) {
QPen cpen = QPen(metricDetail.penColor);
cpen.setWidth(2.0);
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
current->setPen(cpen);
} else if (metricDetail.curveStyle == QwtPlotCurve::Dots) {
sym.setSize(6);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(metricDetail.penColor));
current->setSymbol(sym);
} else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) {
sym.setSize(4);
sym.setPen(QPen(metricDetail.penColor));
sym.setBrush(QBrush(Qt::white));
current->setSymbol(sym);
}
// smoothing
if (metricDetail.smooth == true) {
current->setCurveAttribute(QwtPlotCurve::Fitted, true);
}
// set the data series
current->setData(xdata.data(),ydata.data(), count+1);
current->setBaseline(metricDetail.baseline);
// choose the axis
int axisid = chooseYAxis(metricDetail.uunits);
current->setYAxis(axisid);
// update min/max Y values for the chosen axis
if (current->maxYValue() > maxY[axisid]) maxY[axisid] = current->maxYValue();
if (current->minYValue() < minY[axisid]) minY[axisid] = current->minYValue();
current->attach(this);
// trend - clone the data for the curve and add a curvefitted
// curve with no symbols and use a dashed pen
// need more than 2 points for a trend line
if (metricDetail.trend == true && count > 2) {
QString trendName = QString("%1 trend").arg(metricDetail.uname);
QString trendSymbol = QString("%1_trend").arg(metricDetail.symbol);
QwtPlotCurve *trend = new QwtPlotCurve(trendName);
// cosmetics
QPen cpen = QPen(metricDetail.penColor.darker(200));
cpen.setWidth(4.0);
cpen.setStyle(Qt::DotLine);
trend->setPen(cpen);
trend->setRenderHint(QwtPlotItem::RenderAntialiased);
trend->setBaseline(0);
trend->setYAxis(axisid);
trend->setStyle(QwtPlotCurve::Lines);
// perform linear regression
LTMTrend regress(xdata.data(), ydata.data(), count);
double xtrend[2], ytrend[2];
xtrend[0] = 0.0;
ytrend[0] = regress.getYforX(0.0);
xtrend[1] = xdata[count];
ytrend[1] = regress.getYforX(xdata[count]);
trend->setData(xtrend,ytrend, 2);
trend->attach(this);
curves.insert(trendSymbol, trend);
}
// highlight top N values
if (metricDetail.topN > 0) {
QMap<double, int> sortedList;
// copy the yvalues, retaining the offset
for(int i=0; i<ydata.count(); i++)
sortedList.insert(ydata[i], i);
// copy the top N values
QVector<double> hxdata, hydata;
hxdata.resize(metricDetail.topN);
hydata.resize(metricDetail.topN);
// QMap orders the list so start at the top and work
// backwards
QMapIterator<double, int> i(sortedList);
i.toBack();
int counter = 0;
while (i.hasPrevious() && counter < metricDetail.topN) {
i.previous();
if (ydata[i.value()]) {
hxdata[counter] = xdata[i.value()];
hydata[counter] = ydata[i.value()];
counter++;
}
}
// lets setup a curve with this data then!
QString topName;
if (counter > 1)
topName = QString("%1 Best %2")
.arg(metricDetail.uname)
.arg(counter); // starts from zero
else
topName = QString("Best %1").arg(metricDetail.uname);
QString topSymbol = QString("%1_topN").arg(metricDetail.symbol);
QwtPlotCurve *top = new QwtPlotCurve(topName);
curves.insert(topSymbol, top);
top->setRenderHint(QwtPlotItem::RenderAntialiased);
top->setStyle(QwtPlotCurve::Dots);
// we might have hidden the symbols for this curve
// if its set to none then default to a rectangle
if (metricDetail.symbolStyle == QwtSymbol::NoSymbol)
sym.setStyle(QwtSymbol::Rect);
sym.setSize(12);
QColor lighter = metricDetail.penColor;
lighter.setAlpha(50);
sym.setPen(metricDetail.penColor);
sym.setBrush(lighter);
top->setSymbol(sym);
top->setData(hxdata.data(),hydata.data(), counter);
top->setBaseline(0);
top->setYAxis(axisid);
top->attach(this);
}
}
// setup the xaxis at the bottom
int tics;
maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) -
groupForDate(settings->start.date(), settings->groupBy);
if (maxX < 14) {
tics = 1;
} else {
tics = 1 + maxX/10;
}
setAxisScale(xBottom, -0.5, maxX, tics);
setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(), settings->groupBy), settings->groupBy));
// run through the Y axis
for (int i=0; i<10; i++) {
// set the scale on the axis
if (i != xBottom && i != xTop) {
maxY[i] *= 1.1; // add 10% headroom
setAxisScale(i, minY[i], maxY[i]);
}
}
QString format = axisTitle(yLeft).text();
parent->toolTip()->setAxis(xBottom, yLeft);
parent->toolTip()->setFormat(format);
// draw zone labels axisid of -1 means delete whats there
// cause no watts are being displayed
if (settings->shadeZones == true)
refreshZoneLabels(axes.value("watts", -1));
else
refreshZoneLabels(-1); // turn em off
// plot
replot();
}
void
LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n)
{
QList<SummaryMetrics> *data;
// resize the curve array to maximum possible size
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
- groupForDate(settings->start.date(), settings->groupBy);
x.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
y.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail
// Get metric data, either from metricDB for RideFile metrics
// or from StressCalculator for PM type metrics
QList<SummaryMetrics> PMCdata;
if (metricDetail.type == METRIC_DB || metricDetail.type == METRIC_META) {
data = settings->data;
} else if (metricDetail.type == METRIC_PM) {
createPMCCurveData(settings, metricDetail, PMCdata);
data = &PMCdata;
}
n=-1;
int lastDay=0;
unsigned long secondsPerGroupBy=0;
bool wantZero = (metricDetail.curveStyle == QwtPlotCurve::Steps);
foreach (SummaryMetrics rideMetrics, *data) {
// day we are on
int currentDay = groupForDate(rideMetrics.getRideDate().date(), settings->groupBy);
// value for day
double value = rideMetrics.getForSymbol(metricDetail.symbol);
// check values are bounded to stop QWT going berserk
if (isnan(value) || isinf(value)) value = 0;
// Special computed metrics (LTS/STS) have a null metric pointer
if (metricDetail.metric) {
// convert from stored metric value to imperial
if (useMetricUnits == false) value *= metricDetail.metric->conversion();
// convert seconds to hours
if (metricDetail.metric->units(true) == "seconds") value /= 3600;
}
if (value || wantZero) {
unsigned long seconds = rideMetrics.getForSymbol("workout_time");
if (currentDay > lastDay) {
if (lastDay && wantZero) {
while (lastDay<currentDay) {
lastDay++;
n++;
x[n]=lastDay - groupForDate(settings->start.date(), settings->groupBy);
y[n]=0;
}
} else {
n++;
}
y[n] = value;
x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy);
secondsPerGroupBy = seconds; // reset for new group
} else {
// sum totals, average averages and choose best for Peaks
int type = metricDetail.metric ? metricDetail.metric->type() : RideMetric::Average;
if (metricDetail.uunits == "Ramp") type = RideMetric::Total;
switch (type) {
case RideMetric::Total:
y[n] += value;
break;
case RideMetric::Average:
{
// average should be calculated taking into account
// the duration of the ride, otherwise high value but
// short rides will skew the overall average
y[n] = ((y[n]*secondsPerGroupBy)+(seconds*value)) / (secondsPerGroupBy+seconds);
break;
}
case RideMetric::Peak:
if (value > y[n]) y[n] = value;
break;
}
secondsPerGroupBy += seconds; // increment for same group
}
lastDay = currentDay;
}
}
}
void
LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail,
QList<SummaryMetrics> &customData)
{
QDate earliest, latest; // rides
boost::shared_ptr<QSettings> appsettings = GetApplicationSettings();
QString scoreType;
// create a custom set of summary metric data!
if (metricDetail.name.startsWith("Skiba")) {
scoreType = "skiba_bike_score";
} else if (metricDetail.name.startsWith("Daniels")) {
scoreType = "daniels_points";
}
// create the Stress Calculation List
// FOR ALL RIDE FILES
StressCalculator *sc = new StressCalculator(
settings->start,
settings->end,
(appsettings->value(GC_INITIAL_STS)).toInt(),
(appsettings->value(GC_INITIAL_LTS)).toInt(),
(appsettings->value(GC_STS_DAYS,7)).toInt(),
(appsettings->value(GC_LTS_DAYS,42)).toInt());
sc->calculateStress(main, home.absolutePath(), scoreType);
// pick out any data that is in the date range selected
// convert to SummaryMetric Format used on the plot
for (int i=0; i< sc->n(); i++) {
SummaryMetrics add = SummaryMetrics();
add.setRideDate(settings->start.addDays(i));
if (scoreType == "skiba_bike_score") {
add.setForSymbol("skiba_lts", sc->getLTSvalues()[i]);
add.setForSymbol("skiba_sts", sc->getSTSvalues()[i]);
add.setForSymbol("skiba_sb", sc->getSBvalues()[i]);
add.setForSymbol("skiba_sr", sc->getSRvalues()[i]);
add.setForSymbol("skiba_lr", sc->getLRvalues()[i]);
} else if (scoreType == "daniels_points") {
add.setForSymbol("daniels_lts", sc->getLTSvalues()[i]);
add.setForSymbol("daniels_sts", sc->getSTSvalues()[i]);
add.setForSymbol("daniels_sb", sc->getSBvalues()[i]);
add.setForSymbol("daniels_sr", sc->getSRvalues()[i]);
add.setForSymbol("daniels_lr", sc->getLRvalues()[i]);
}
add.setForSymbol("workout_time", 1.0); // averaging is per day
customData << add;
}
delete sc;
}
int
LTMPlot::chooseYAxis(QString units)
{
int chosen;
// return the YAxis to use
if ((chosen = axes.value(units, -1)) != -1) return chosen;
else if (axes.count() < 8) {
chosen = supported_axes[axes.count()];
if (units == "seconds") setAxisTitle(chosen, "hours"); // we convert seconds to hours
else setAxisTitle(chosen, units);
enableAxis(chosen, true);
axes.insert(units, chosen);
return chosen;
} else {
// eek!
return yLeft; // just re-use the current yLeft axis
}
}
int
LTMPlot::groupForDate(QDate date, int groupby)
{
switch(groupby) {
case LTM_WEEK:
{
// must start from 1 not zero!
return 1 + ((date.toJulianDay() - settings->start.date().toJulianDay()) / 7);
}
case LTM_MONTH: return (date.year()*12) + date.month();
case LTM_YEAR: return date.year();
case LTM_DAY:
default:
return date.toJulianDay();
}
}
void
LTMPlot::pointHover(QwtPlotCurve *curve, int index)
{
if (index >= 0 && curve != highlighter) {
const RideMetricFactory &factory = RideMetricFactory::instance();
double value;
QString units;
int precision;
QString datestr;
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start, groupForDate(settings->start.date(), settings->groupBy), settings->groupBy);
QwtText startText = lsd->label((int)(curve->x(index)+0.5));
QwtText endText;
endText = lsd->label((int)(curve->x(index)+1.5));
if (settings->groupBy != LTM_WEEK)
datestr = startText.text();
else
datestr = QString("%1 - %2").arg(startText.text()).arg(endText.text());
datestr = datestr.replace('\n', ' ');
// we reference the metric definitions of name and
// units to decide on the level of precision required
QHashIterator<QString, QwtPlotCurve*> c(curves);
while (c.hasNext()) {
c.next();
if (c.value() == curve) {
const RideMetric *metric =factory.rideMetric(c.key());
units = metric ? metric->units(useMetricUnits) : "";
precision = metric ? metric->precision() : 1;
// BikeScore, RI and Daniels Points have no units
if (units == "" && metric != NULL) {
QTextEdit processHTML(factory.rideMetric(c.key())->name());
units = processHTML.toPlainText();
}
break;
}
}
// the point value
value = curve->y(index);
// convert seconds to hours for the LTM plot
if (units == "seconds") {
units = "hours"; // we translate from seconds to hours
value = ceil(curve->y(index)*10.0)/10.0;
precision = 1; // new more precision since converting to hours
}
// output the tooltip
QString text = QString("%1\n%2\n%3 %4")
.arg(datestr)
.arg(curve->title().text())
.arg(value, 0, 'f', precision)
.arg(this->axisTitle(curve->yAxis()).text());
// set that text up
parent->toolTip()->setText(text);
} else {
// no point
parent->toolTip()->setText("");
}
}
// start of date range selection
void
LTMPlot::pickerAppended(QPoint pos)
{
// ony work once we have a chart to do it on
if (settings == NULL) return;
// allow user to select a date range across the plot
if (highlighter) {
// detach and delete
highlighter->detach();
delete highlighter;
}
highlighter = new QwtPlotCurve("Date Selection");
double curveDataX[4]; // simple 4 point line
double curveDataY[4]; // simple 4 point line
// get x
int x = invTransform(xBottom, pos.x());
// trying to select a range on anull plot
if (maxY[yLeft] == 0) {
enableAxis(yLeft, true);
setAxisTitle(yLeft, tr("watts")); // as good as any
setAxisScale(yLeft, 0, 1000);
maxY[yLeft] = 1000;
}
// get min/max y
curveDataX[0]=x;
curveDataY[0]=minY[yLeft];
curveDataX[1]=x;
curveDataY[1]=maxY[yLeft];
// no right then down - updated by pickerMoved
curveDataX[2]=curveDataX[1];
curveDataY[2]=curveDataY[1];
curveDataX[3]=curveDataX[0];
curveDataY[3]=curveDataY[3];
// color
QColor ccol(GColor(CINTERVALHIGHLIGHTER));
ccol.setAlpha(64);
QPen cpen = QPen(ccol);
cpen.setWidth(1.0);
QBrush cbrush = QBrush(ccol);
highlighter->setPen(cpen);
highlighter->setBrush(cbrush);
highlighter->setStyle(QwtPlotCurve::Lines);
highlighter->setData(curveDataX,curveDataY, 4);
// axis etc
highlighter->setYAxis(QwtPlot::yLeft);
highlighter->attach(this);
highlighter->show();
// what is the start date?
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(),
settings->groupBy),
settings->groupBy);
start = lsd->toDate((int)x);
end = start.addYears(10);
name = QString("%1 - ").arg(start.toString("d MMM yy"));
seasonid = settings->ltmTool->newSeason(name, start, end, Season::adhoc);
replot();
}
// end of date range selection
void
LTMPlot::pickerMoved(QPoint pos)
{
if (settings == NULL) return;
// allow user to select a date range across the plot
double curveDataX[4]; // simple 4 point line
double curveDataY[4]; // simple 4 point line
// get x
int x = invTransform(xBottom, pos.x());
// update to reflect new x position
curveDataX[0]=highlighter->x(0);
curveDataY[0]=highlighter->y(0);
curveDataX[1]=highlighter->x(0);
curveDataY[1]=highlighter->y(1);
curveDataX[2]=x;
curveDataY[2]=curveDataY[1];
curveDataX[3]=x;
curveDataY[3]=curveDataY[3];
// what is the end date?
LTMScaleDraw *lsd = new LTMScaleDraw(settings->start,
groupForDate(settings->start.date(),
settings->groupBy),
settings->groupBy);
end = lsd->toDate((int)x);
name = QString("%1 - %2").arg(start.toString("d MMM yy"))
.arg(end.toString("d MMM yy"));
settings->ltmTool->updateSeason(seasonid, name, start, end, Season::adhoc);
// update and replot highlighter
highlighter->setData(curveDataX,curveDataY, 4);
replot();
}
/*----------------------------------------------------------------------
* Draw Power Zone Shading on Background (here to end of source file)
*
* THANKS TO DAMIEN GRAUSER FOR GETTING THIS WORKING TO SHOW
* ZONE SHADING OVER TIME. WHEN CP CHANGES THE ZONE SHADING AND
* LABELLING CHANGES TOO. NEAT.
*--------------------------------------------------------------------*/
class LTMPlotBackground: public QwtPlotItem
{
private:
LTMPlot *parent;
public:
LTMPlotBackground(LTMPlot *_parent, int axisid)
{
setAxis(QwtPlot::xBottom, axisid);
setZ(0.0);
parent = _parent;
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
virtual void draw(QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
const QRect &rect) const
{
const Zones *zones = parent->parent->main->zones();
int zone_range_size = parent->parent->main->zones()->getRangeSize();
//fprintf(stderr, "size: %d\n",zone_range_size);
if (zone_range_size >= 0) { //parent->shadeZones() &&
for (int i = 0; i < zone_range_size; i ++) {
int zone_range = i;
//int zone_range = zones->whichRange(parent->settings->start.addDays((parent->settings->end.date().toJulianDay()-parent->settings->start.date().toJulianDay())/2).date()); // XXX Damien fixup
int left = xMap.transform(parent->groupForDate(zones->getStartDate(zone_range), parent->settings->groupBy)- parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
//fprintf(stderr, "%d left: %d\n",i,left);
//int right = xMap.transform(parent->groupForDate(zones->getEndDate(zone_range), parent->settings->groupBy)- parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
//fprintf(stderr, "%d right: %d\n",i,right);
/* The +50 pixels is for a QWT bug? cover the little gap on the right? */
int right = xMap.transform(parent->maxX + 0.5) + 50;
if (right<0)
right= xMap.transform(parent->groupForDate(parent->settings->end.date(), parent->settings->groupBy) - parent->groupForDate(parent->settings->start.date(), parent->settings->groupBy));
QList <int> zone_lows = zones->getZoneLows(zone_range);
int num_zones = zone_lows.size();
if (num_zones > 0) {
for (int z = 0; z < num_zones; z ++) {
QRect r = rect;
r.setLeft(left);
r.setRight(right);
QColor shading_color = zoneColor(z, num_zones);
shading_color.setHsv(
shading_color.hue(),
shading_color.saturation() / 4,
shading_color.value()
);
r.setBottom(yMap.transform(zone_lows[z]));
if (z + 1 < num_zones)
r.setTop(yMap.transform(zone_lows[z + 1]));
if (r.top() <= r.bottom())
painter->fillRect(r, shading_color);
}
}
}
}
}
};
// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class LTMPlotZoneLabel: public QwtPlotItem
{
private:
LTMPlot *parent;
int zone_number;
double watts;
QwtText text;
public:
LTMPlotZoneLabel(LTMPlot *_parent, int _zone_number, int axisid, LTMSettings *settings)
{
parent = _parent;
zone_number = _zone_number;
const Zones *zones = parent->parent->main->zones();
//int zone_range = 0; //parent->parent->mainWindow->zoneRange();
int zone_range = zones->whichRange(settings->start.addDays((settings->end.date().toJulianDay()-settings->start.date().toJulianDay())/2).date()); // XXX Damien Fixup
// which axis has watts?
setAxis(QwtPlot::xBottom, axisid);
// create new zone labels if we're shading
if (zone_range >= 0) { //parent->shadeZones()
QList <int> zone_lows = zones->getZoneLows(zone_range);
QList <QString> zone_names = zones->getZoneNames(zone_range);
int num_zones = zone_lows.size();
assert(zone_names.size() == num_zones);
if (zone_number < num_zones) {
watts =
(
(zone_number + 1 < num_zones) ?
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
(
(zone_number > 0) ?
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
2.0 * zone_lows[zone_number]
)
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
}
}
setZ(1.0 + zone_number / 100.0);
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRect &rect) const
{
if (true) {//parent->shadeZones()
int x = (rect.left() + rect.right()) / 2;
int y = yMap.transform(watts);
// the following code based on source for QwtPlotMarker::draw()
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
tr.moveCenter(QPoint(x, y));
text.draw(painter, tr);
}
}
};
void
LTMPlot::refreshZoneLabels(int axisid)
{
foreach(LTMPlotZoneLabel *label, zoneLabels) {
label->detach();
delete label;
}
zoneLabels.clear();
if (bg) {
bg->detach();
delete bg;
bg = NULL;
}
if (axisid == -1) return; // our job is done - no zones to plot
const Zones *zones = main->zones();
if (zones == NULL || zones->getRangeSize()==0) return; // no zones to plot
int zone_range = 0; // first range
// generate labels for existing zones
if (zone_range >= 0) {
int num_zones = zones->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
LTMPlotZoneLabel *label = new LTMPlotZoneLabel(this, z, axisid, settings);
label->attach(this);
zoneLabels.append(label);
}
}
bg = new LTMPlotBackground(this, axisid);
bg->attach(this);
}