/* * 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 "IntervalItem.h" #include "Context.h" #include "Context.h" #include "Athlete.h" #include "Settings.h" #include "Zones.h" #include "Colors.h" #include "RideFile.h" #include "Units.h" // for MILES_PER_KM #include #include #include #include #include #include #include #include #include #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 ? } QString ScatterPlot::describeType(int type, bool longer, bool useMetricUnits) { // return the point value for the given type if (longer == true) { switch(type) { case MODEL_POWER : return (tr("Power (watts)")); case MODEL_CADENCE : return (tr("Cadence (rpm)")); case MODEL_HEARTRATE : return (tr("Heartrate (bpm)")); case MODEL_SPEED : if (useMetricUnits == true){ return (tr("Speed (kph)")); }else { return (tr("Speed (mph)")); } case MODEL_ALT : if (useMetricUnits == true){ return (tr("Altitude (meters)")); }else { return (tr("Altitude (feet)")); } case MODEL_TORQUE : return (tr("Torque (N)")); case MODEL_TIME : return (tr("Elapsed Time (secs)")); case MODEL_DISTANCE : if (useMetricUnits == true){ return (tr("Elapsed Distance (km)")); }else { return (tr("Elapsed Distance (mi)")); } case MODEL_INTERVAL : return (tr("Interval Number")); case MODEL_LAT : return (tr("Latitude (degree offset)")); case MODEL_LONG : return (tr("Longitude (degree offset)")); case MODEL_CPV : return (tr("Circumferential Pedal Velocity (cm/s)")); case MODEL_AEPF : return (tr("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 (tr("Time at X/Y (%)")); case MODEL_POWERZONE : return (tr("Power Zone")); } return (tr("Unknown"));; // ? unknown channel ? } else { switch(type) { case MODEL_POWER : return (tr("Power")); case MODEL_CADENCE : return (tr("Cadence")); case MODEL_HEARTRATE : return (tr("Heartrate")); case MODEL_SPEED : return (tr("Speed")); case MODEL_ALT : return (tr("Altitude")); case MODEL_TORQUE : return (tr("Pedal Force")); case MODEL_TIME : return (tr("Time")); case MODEL_DISTANCE : return (tr("Distance")); case MODEL_INTERVAL : return (tr("Interval")); case MODEL_LAT : return (tr("Latitude")); case MODEL_LONG : return (tr("Longitude")); case MODEL_XYTIME : return (tr("Time at X/Y")); case MODEL_POWERZONE : return (tr("Zone")); case MODEL_CPV : return (tr("CPV")); case MODEL_AEPF : return (tr("AEPF")); } return (tr("None")); // ? unknown channel ? } } ScatterPlot::ScatterPlot(Context *context) : context(context) { 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(context, 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; // 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, context->athlete->useMetricUnits, cranklength); double yv = y[points] = pointType(point, settings->y, context->athlete->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)); QPen p; p.setColor(GColor(CPLOTSYMBOL)); sym.setPen(p); // wipe away existing if (all) { all->detach(); delete all; } // setup the framing curve if (settings->frame) { all = new QwtPlotCurve(); all->setSymbol(new QwtSymbol(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 i(intervalCurves); while (i.hasNext()) { QwtPlotCurve *curve = i.next(); curve->detach(); delete curve; } } intervalCurves.clear(); // which ones are highlighted then? QVector intervals; QMap displaySequence; for (int child=0; childathlete->allIntervalItems()->childCount(); child++) { IntervalItem *current = dynamic_cast(context->athlete->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 > xvals(intervals.count()); // array of curve x arrays QVector > yvals(intervals.count()); // array of curve x arrays QVector 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(context->athlete->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 order(displaySequence); while (order.hasNext()) { order.next(); int idx = order.value(); QPen pen; QColor intervalColor; intervalColor.setHsv((255/context->athlete->allIntervalItems()->childCount()) * (intervals[idx]), 255,255); pen.setColor(intervalColor); sym.setPen(pen); QwtPlotCurve *curve = new QwtPlotCurve(); curve->setSymbol(new QwtSymbol(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)); } 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 && settings->ride && 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(new QwtSymbol(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); }