/*
 * Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
 *                    J.T Conklin (jtc@acorntoolworks.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 "PfPvPlot.h"
#include "Athlete.h"
#include "Context.h"
#include "RideFile.h"
#include "RideItem.h"
#include "IntervalItem.h"
#include "Settings.h"
#include "Zones.h"
#include "Colors.h"

#include <cmath>
#include <qwt_series_data.h>
#include <qwt_legend.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_marker.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_widget.h>
#include <qwt_scale_map.h>
#include <qwt_symbol.h>
#include <set>

#define PI M_PI


// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class PfPvPlotZoneLabel: public QwtPlotItem
{

private:

    PfPvPlot *parent;
    int zone_number;
    double watts;
    QwtText text;

public:

    PfPvPlotZoneLabel(PfPvPlot *_parent, int _zone_number)
    {
        parent = _parent;
        RideItem *rideItem = parent->rideItem;
        zone_number = _zone_number;

        // get zone data from ride or athlete ...
        const Zones *zones;
        int zone_range = -1;

        if (parent->context->isCompareIntervals) {

            zones = parent->context->athlete->zones(rideItem ? rideItem->sport : "Bike");
            if (!zones) return;

            // use first compare interval date
            if (parent->context->compareIntervals.count())
                zone_range = zones->whichRange(parent->context->compareIntervals[0].data->startTime().date());

            // still not set 
            if (zone_range == -1)
                zone_range = zones->whichRange(QDate::currentDate());

        } else if (rideItem && parent->context->athlete->zones(rideItem->sport)) {

            zones = parent->context->athlete->zones(rideItem->sport);
            zone_range = zones->whichRange(rideItem->dateTime.date());

        } else {

            return; // nulls

        }

        setZ(1.0 + zone_number / 100.0);

        // create new zone labels if we're shading
        if (zone_range >= 0) {

            // retrieve zone setup
            QList <int> zone_lows = zones->getZoneLows(zone_range);
            QList <QString> zone_names = zones->getZoneNames(zone_range);
            int num_zones = zone_lows.size();
            if(zone_names.size() != num_zones) return;

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

    virtual int rtti() const {
        return QwtPlotItem::Rtti_PlotUserItem;
    }

    void draw(QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap, const QRectF &rect) const {

        if (parent->shadeZones() && (rect.width() > 0) && (rect.height() > 0)) {
            // draw the label along a plot diagonal:
            // 1. x*y = watts * dx/dv * dy/df
            // 2. x/width = y/height
            // =>
            // 1. x^2 = width/height * watts
            // 2. y^2 = height/width * watts
            double xscale = fabs(xMap.transform(parent->maxCPV) - xMap.transform(0)) / parent->maxCPV;
            double yscale = fabs(yMap.transform(parent->maxAEPF) - yMap.transform(0)) / parent->maxAEPF;
            if ((xscale > 0) && (yscale > 0)) {
                double w = watts * xscale * yscale;
                int x = xMap.transform(sqrt(w * rect.width() / rect.height()) / xscale);
                int y = yMap.transform(sqrt(w * rect.height() / rect.width()) / yscale);

                // the following code based on source for QwtPlotMarker::draw()
                QRect tr(QPoint(0, 0), text.textSize(painter->font()).toSize());
                tr.moveCenter(QPoint(x, y));
                text.draw(painter, tr);
            }
        }
    }
};


PfPvPlot::PfPvPlot(Context *context)
    : rideItem (NULL), context(context), hover(NULL), cp_ (0), pmax_(0), cad_ (85), cl_ (0.175), shade_zones(true)
{
    static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);

    setAutoFillBackground(true);
    setAxisTitle(QwtAxis::YLeft, tr("Average Effective Pedal Force (N)"));
    setAxisScale(QwtAxis::YLeft, 0, 600);
    setAxisTitle(QwtAxis::XBottom, tr("Circumferential Pedal Velocity (m/s)"));
    setAxisScale(QwtAxis::XBottom, 0, 3);
    setAxisMaxMinor(QwtAxis::YLeft, 0);
    setAxisMaxMinor(QwtAxis::XBottom, 0);
    QwtScaleDraw *sd = new QwtScaleDraw;
    sd->setTickLength(QwtScaleDiv::MajorTick, 3);
    setAxisScaleDraw(QwtAxis::XBottom, sd);
    sd = new QwtScaleDraw;
    sd->setTickLength(QwtScaleDiv::MajorTick, 3);
    sd->enableComponent(QwtScaleDraw::Ticks, false);
    sd->enableComponent(QwtScaleDraw::Backbone, false);
    setAxisScaleDraw(QwtAxis::YLeft, sd);

    mX = new QwtPlotMarker();
    mX->setLineStyle(QwtPlotMarker::VLine);
    mX->attach(this);

    mY = new QwtPlotMarker();
    mY->setLineStyle(QwtPlotMarker::HLine);
    mY->attach(this);

    cpCurve = new QwtPlotCurve();
    cpCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
    cpCurve->attach(this);

    pmaxCurve = new QwtPlotCurve();
    pmaxCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
    pmaxCurve->attach(this);

    curve = new QwtPlotCurve();
    curve->attach(this);

    cl_ = appsettings->cvalue(context->athlete->cyclist, GC_CRANKLENGTH).toDouble() / 1000.0;

    // markup timeInQuadrant
    tiqMarker[0] = new QwtPlotMarker(); tiqMarker[0]->attach(this);
    tiqMarker[0]->setXValue(2.9);
    tiqMarker[0]->setYValue(580);

    tiqMarker[1] = new QwtPlotMarker(); tiqMarker[1]->attach(this);
    tiqMarker[1]->setXValue(0.1);
    tiqMarker[1]->setYValue(580);

    tiqMarker[2] = new QwtPlotMarker(); tiqMarker[2]->attach(this);
    tiqMarker[2]->setXValue(0.1);
    tiqMarker[2]->setYValue(10);

    tiqMarker[3] = new QwtPlotMarker(); tiqMarker[3]->attach(this);
    tiqMarker[3]->setXValue(2.9);
    tiqMarker[3]->setYValue(10);

    merge_intervals = false;
    frame_intervals = true;
    gear_ratio_display = false;

    // only default on first time through, after this the user may have adjusted
    if (appsettings->value(this, GC_SHADEZONES, true).toBool()==false) shade_zones = false;
    else shade_zones = true;

    configChanged(CONFIG_APPEARANCE);

    recalc();
}

void
PfPvPlot::configChanged(qint32)
{
    setCanvasBackground(GColor(CPLOTBACKGROUND));

    // frame with inverse of background
    QwtSymbol *sym = new QwtSymbol;
    sym->setStyle(QwtSymbol::Ellipse);
    sym->setSize(4*dpiXFactor);
    sym->setPen(QPen(Qt::red));
    sym->setBrush(QBrush(Qt::red));
    curve->setSymbol(sym);
    curve->setStyle(QwtPlotCurve::Dots);
    curve->setRenderHint(QwtPlotItem::RenderAntialiased);

    QPalette palette;
    palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
    palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
    palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
    setPalette(palette);

    axisWidget(QwtAxis::XBottom)->setPalette(palette);
    axisWidget(QwtAxis::YLeft)->setPalette(palette);

    // use grid line color for mX, mY and CPcurve
    QPen marker = GColor(CPLOTMARKER);
    QPen cp = GColor(CCP);
    mX->setLinePen(marker);
    mY->setLinePen(marker);
    cpCurve->setPen(cp);

    setCL(appsettings->cvalue(context->athlete->cyclist, GC_CRANKLENGTH).toDouble() / 1000.0);
    QPen pmax = GColor(CCP);
    pmax.setStyle(Qt::DashLine);
    pmaxCurve->setPen(pmax);

    replot();
}

void
PfPvPlot::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);
}

void
PfPvPlot::refreshZoneItems()
{
    // clear out any zone curves which are presently defined
    if (zoneCurves.size()) {

        QListIterator<QwtPlotCurve *> i(zoneCurves);
        while (i.hasNext()) {
            QwtPlotCurve *curve = i.next();
            curve->detach();
            //delete curve;
        }
    }
    zoneCurves.clear();

    // delete any existing power zone labels
    if (zoneLabels.size()) {

        QListIterator<PfPvPlotZoneLabel *> i(zoneLabels);
        while (i.hasNext()) {
            PfPvPlotZoneLabel *label = i.next();
            label->detach();
            delete label;
        }
    }
    zoneLabels.clear();

    // set zones from ride or athlete and date of
    // first item in the compare set
    const Zones *zones = context->athlete->zones(rideItem ? rideItem->sport : "Bike");
    int zone_range = -1;

    // comparing does zones for items selected not current ride
    if (context->isCompareIntervals) {

        // no athlete zones anyway!
        if (!zones) return;

        // use first compare interval date
        if (context->compareIntervals.count()) {
            zone_range = zones->whichRange(context->compareIntervals[0].data->startTime().date());
        }

        // still not set 
        if (zone_range == -1) {
            zone_range = zones->whichRange(QDate::currentDate());
        }

    } else if (rideItem && zones) {

        zone_range = zones->whichRange(rideItem->dateTime.date());

    } else {

        return; // null ride and not compare etc
    }

    if (zone_range >= 0) {
        setCP(zones->getCP(zone_range));
        setPMax(zones->getPmax(zone_range));

        // populate the zone curves
        QList <int> zone_power = zones->getZoneLows(zone_range);
        QList <QString> zone_name = zones->getZoneNames(zone_range);
        int num_zones = zone_power.size();
        if (zone_name.size() != num_zones) return;

        if (num_zones > 0) {
            QPen *pen = new QPen();
            pen->setStyle(Qt::NoPen);

            QVector<double> yvalues;

            // generate x values
            for (int z = 0; z < num_zones; z ++) {

                QwtPlotCurve *curve = new QwtPlotCurve(zone_name[z]);

                curve->setPen(*pen);
                QColor brush_color = zoneColor(z, num_zones);
                brush_color.setHsv(brush_color.hue(), brush_color.saturation() / 4, brush_color.value());
                curve->setBrush(brush_color);   // fill below the line
                curve->setZ(1 - 1e-6 * zone_power[z]);

                // generate data for curve
                if (z < num_zones - 1) {
                    QVector <double> contour_yvalues;
                    int watts = zone_power[z + 1];
                    int dwatts = (double) watts;
                    for (int i = 0; i < contour_xvalues.size(); i ++) {
                        contour_yvalues.append( (1e6 * contour_xvalues[i] < watts) ?  1e6 : dwatts / contour_xvalues[i]);
                    }
                    curve->setSamples(contour_xvalues, contour_yvalues);

                } else {

                    // top zone has a curve at "infinite" power
                    QVector <double> contour_x;
                    QVector <double> contour_y;
                    contour_x.append(contour_xvalues[0]);
                    contour_x.append(contour_xvalues[contour_xvalues.size() - 1]);
                    contour_y.append(1e6);
                    contour_y.append(1e6);
                    curve->setSamples(contour_x, contour_y);
                }

                curve->setVisible(shade_zones);
                curve->attach(this);
                zoneCurves.append(curve);
            }

            delete pen;

            // generate labels for existing zones
            for (int z = 0; z < num_zones; z ++) {
                PfPvPlotZoneLabel *label = new PfPvPlotZoneLabel(this, z);
                label->setVisible(shade_zones);
                label->attach(this);
                zoneLabels.append(label);
            }
        }
    }
}


// how many intervals selected?
int PfPvPlot::intervalCount() const
{
    return rideItem->intervalsSelected().count();
}

void 
PfPvPlot::mouseTrack(double cad, double watts)
{
    if (watts <= 0 || cad <= 0) return;

    double aepf = (watts * 60.0) / (cad * cl_ * 2.0 * PI);
    double cpv = (cad * cl_ * 2.0 * PI) / 60.0;

    if (rideItem && rideItem->ride() && rideItem->intervals().count() >= intervalMarkers.count()) {
        // are we hovering "close" to an interval marker ?
        int index = 0;
        foreach (QwtPlotMarker *is, intervalMarkers) {

            double x = is->xValue() - cpv;
            double y = is->yValue() - aepf;

            if ((x > -0.05f && x < 0.05f) && (y > -3 && y < 3)) {
                context->notifyIntervalHover(rideItem->intervals()[index]);
            }
    
            index++;
        }
    }
}

// for accumulating averages when refreshing interval markers
class accum { public: accum() : count(0), aepf(0), cpv(0) {} int count; double aepf; double cpv; };

void
PfPvPlot::refreshIntervalMarkers()
{
    // zap what we got ...
    foreach(QwtPlotMarker *is, intervalMarkers) {
        is->detach();
        delete is;
    }
    intervalMarkers.clear();

    // do we have a ride with intervals to refresh ?
    int count=0;
    if (rideItem && rideItem->ride() && rideItem->ride()->dataPoints().count() && (count = rideItem->intervals().count())) {

        // accumulating...
        QVector<accum> intervalAccumulator(count);

        foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) {

            if (p1->cad && p1->watts) {

                // calculate the values
                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                // accumulate values for each interval here ....
                for(int i=0; i < count; i++) {

                    IntervalItem *v = rideItem->intervals()[i];

                    // in our interval ?
                    if (p1->secs >= v->start && p1->secs <= v->stop) {
                        intervalAccumulator[i].aepf += aepf;
                        intervalAccumulator[i].cpv += cpv;
                        intervalAccumulator[i].count++;
                    }
                }
            }
        }

        // ok, so now add markers for the average position for each interval
        int index = 0;
        foreach (accum a, intervalAccumulator) {

            QColor color;
            color.setHsv(index * 255/count, 255,255);
            double cpv = a.cpv/double(a.count);
            double aepf = a.aepf/double(a.count);

            QwtSymbol *sym = new QwtSymbol;
            sym->setStyle(QwtSymbol::Diamond);
            sym->setSize(8*dpiXFactor);
            sym->setPen(QPen(GColor(CPLOTMARKER)));
            sym->setBrush(QBrush(color));

            QwtPlotMarker *p = new QwtPlotMarker();
            p->setValue(cpv, aepf);
            p->setYAxis(QwtAxis::YLeft);
            p->setSymbol(sym);
            p->attach(this);
            intervalMarkers << p;

            index++;
        }
    }
}

// XXX use of make_pair is shitty since we have QT containers
// but rather than refactor here we just fixup for c++11 and
// MSVC syntax. The pfpv plot will go when we introduce the
// new scatter plot in v4 (hopefully)
#if __cplusplus >= 201103L
#define MAKEPAIR pair
#else
#define MAKEPAIR make_pair
#endif

void
PfPvPlot::setData(RideItem *_rideItem)
{
    if (context->isCompareIntervals) return;

    // 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();

    // clear the hover curve
    if (hover) {
        hover->detach();
        delete hover;
        hover = NULL;
    }

    rideItem = _rideItem;
    RideFile *ride = rideItem->ride();

    if (ride) {

        // quickly erase old data
        mainCurvesSetVisible(false);


        // due to the discrete power and cadence values returned by the
        // power meter, there will very likely be many duplicate values.
        // Rather than pass them all to the curve, use a set to strip
        // out duplicates.
        std::set<std::pair<double, double> > dataSet;

        const int num_gearRatio_Plots = 4;
        QVector<std::set<std::pair<double, double> > > gearSet(num_gearRatio_Plots);
        long tot_cad = 0;
        long tot_cad_points = 0;


        foreach(const RideFilePoint *p1, ride->dataPoints()) {

            if (p1->watts != 0 && p1->cad != 0) {

                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                if (aepf <= 2500) { // > 2500 newtons is our out of bounds
#if Q_CC_MSVC
                    dataSet.insert(std::make_pair(aepf, cpv));
#else
                    dataSet.insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif

                    tot_cad += p1->cad;
                    tot_cad_points++;

                    // fill the dataSets for gearRatio display
                    if (p1->gear != 0) {
                        if (p1->gear > 0.0f && p1->gear <= 1.00f) {
#if Q_CC_MSVC
                            gearSet[0].insert(std::make_pair(aepf, cpv));
#else
                            gearSet[0].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                        } else if (p1->gear > 1.00f && p1->gear <= 2.50f ) {
#if Q_CC_MSVC
                            gearSet[1].insert(std::make_pair(aepf, cpv));
#else
                            gearSet[1].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                        } else if (p1->gear > 2.50f && p1->gear <= 4.00 ) {
#if Q_CC_MSVC
                            gearSet[2].insert(std::make_pair(aepf, cpv));
#else
                            gearSet[2].insert(std::make_pair(aepf, cpv));
#endif
                        } else {  // > 4.0
#if Q_CC_MSVC
                            gearSet[3].insert(std::make_pair(aepf, cpv));
#else
                            gearSet[3].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                        }
                    }
                 }
            }
        }

        setCAD(tot_cad_points ? tot_cad / tot_cad_points : 0);

        if (tot_cad_points == 0) {
            //setTitle(tr("no cadence"));
            refreshZoneItems();
            mainCurvesSetVisible(false);

        } else {
            // Now that we have the set of points, transform them into the
            // QwtArrays needed to set the curve's data.
            QVector<double> aepfArray;
            QVector<double> cpvArray;

            std::set<std::pair<double, double> >::const_iterator j(dataSet.begin());
            while (j != dataSet.end()) {
                const std::pair<double, double>& dataPoint = *j;

                aepfArray.push_back(dataPoint.first);
                cpvArray.push_back(dataPoint.second);

                ++j;
            }

            curve->setSamples(cpvArray, aepfArray);

            QwtSymbol *sym = new QwtSymbol;
            sym->setStyle(QwtSymbol::Ellipse);
            sym->setSize(4*dpiXFactor);
            sym->setPen(QPen(Qt::red));
            sym->setBrush(QBrush(Qt::red));
            curve->setSymbol(sym);
            curve->setStyle(QwtPlotCurve::Dots);
            curve->setRenderHint(QwtPlotItem::RenderAntialiased);

            // now show the data (zone shading would already be visible)
            refreshZoneItems();

            // averages for intervals
            refreshIntervalMarkers();

            // now the curves for gearRatio
            if (gearRatioCurves.size()) {
               QListIterator<QwtPlotCurve *> g(gearRatioCurves);
               while (g.hasNext()) {
                   QwtPlotCurve *curve = g.next();
                   curve->detach();
                   delete curve;
               }
            }
            gearRatioCurves.clear();

            // get the detailed data
            QVector<QVector<double> > aepfArrayGearRatio(num_gearRatio_Plots);
            QVector<QVector<double> > cpvArrayGearRatio(num_gearRatio_Plots);

            for (int i=0;i<num_gearRatio_Plots;i++) {
                std::set<std::pair<double, double> >::const_iterator m(gearSet[i].begin());
                while (m != gearSet[i].end()) {
                    const std::pair<double, double>& dataPoint = *m;

                    aepfArrayGearRatio[i].push_back(dataPoint.first);
                    cpvArrayGearRatio[i].push_back(dataPoint.second);

                    ++m;
                }
            }

            // create the plots
            for (int i=0;i<num_gearRatio_Plots;i++) {
                QwtPlotCurve *gearCurve;
                gearCurve = new QwtPlotCurve();
                QwtSymbol *sym = new QwtSymbol;
                sym->setStyle(QwtSymbol::Diamond);
                sym->setSize(3*dpiXFactor);
                sym->setBrush(QBrush(Qt::NoBrush));
                sym->setPen(QPen(Qt::red));
                switch (i) {
                  case 0: sym->setPen(QPen(Qt::red));  sym->setBrush(QBrush(Qt::red));  break;
                  case 1: sym->setPen(QPen(Qt::yellow)); sym->setBrush(QBrush(Qt::yellow));break;
                  case 2: sym->setPen(QPen(Qt::green));  sym->setBrush(QBrush(Qt::green));break;
                  case 3: sym->setPen(QPen(Qt::blue));  sym->setBrush(QBrush(Qt::blue)); break;
                    // default means something is wrong, but no err
                  default: sym->setPen(QPen(Qt::white)); sym->setBrush(QBrush(Qt::white));
                }
                gearCurve->setSymbol(sym);
                gearCurve->setStyle(QwtPlotCurve::Dots);
                gearCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
                gearCurve->setSamples(cpvArrayGearRatio[i], aepfArrayGearRatio[i]);
                gearCurve->attach(this);

                gearRatioCurves.append(gearCurve);

            }

        }
        mainCurvesSetVisible(true);

    } else {

        //setTitle("no data");
        refreshZoneItems();
        mainCurvesSetVisible(false);
    }

    replot();
}

void
PfPvPlot::intervalHover(IntervalItem *x)
{
    if (!isVisible()) return;
    if (context->isCompareIntervals) return;
    if (!rideItem) return;
    if (!rideItem->ride()) return;

    // zap the old one
    if (hover) {
        hover->detach();
        delete hover;
        hover = NULL;
    }

    // if hovering AND no intervals are selected then lets do it
    if (x && rideItem->intervalsSelected().count() == 0) {

        // collect the data
        QVector<double> aepfArray, cpvArray;
        foreach(const RideFilePoint *p1, rideItem->ride()->dataPoints()) {

            if (p1->secs < x->start || p1->secs > x->stop) continue;

            if (p1->watts != 0 && p1->cad != 0) {
                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                aepfArray << aepf;
                cpvArray << cpv;
            }
        }

        // which interval is it or how many ?
        int count = 0;
        int ours = 0;
        foreach(IntervalItem *p, rideItem->intervals()) {
            if (p->start == x->start && p->stop == x->stop) ours = count;
            count++;
        }

        // any data ?
        if (count > 0 && aepfArray.size()) {
            QwtSymbol *sym = new QwtSymbol;
            sym->setStyle(QwtSymbol::Ellipse);
            sym->setSize(4*dpiXFactor);
            QColor color;
            color.setHsv(ours * 255/count, 255,255);
            color.setAlpha(128);
            sym->setPen(QPen(color));
            sym->setBrush(QBrush(color));

            hover = new QwtPlotCurve();
            hover->setSymbol(sym);
            hover->setStyle(QwtPlotCurve::Dots);
            hover->setRenderHint(QwtPlotItem::RenderAntialiased);
            hover->setSamples(cpvArray, aepfArray);
            hover->attach(this);
        }
    }

    replot(); // refresh
}

void
PfPvPlot::showIntervals(RideItem *_rideItem)
{
    if (context->isCompareIntervals) return;
    if (!rideItem) return;

    // 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();

    rideItem = _rideItem;

    RideFile *ride = rideItem->ride();

    if (ride) {
       int num_intervals=intervalCount();

       if (mergeIntervals()) num_intervals = 1;
       if (frameIntervals() || num_intervals==0) mainCurvesSetVisible(true);
       if (frameIntervals()==false && num_intervals) mainCurvesSetVisible(false);
       QVector<std::set<std::pair<double, double> > > dataSetInterval(num_intervals);

       long tot_cad_points = 0;

        foreach(const RideFilePoint *p1, ride->dataPoints()) {

            if (p1->watts != 0 && p1->cad != 0) {
                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                for (int high=-1, t=0; t<rideItem->intervals().count(); t++) {

                    IntervalItem *current = dynamic_cast<IntervalItem *>(rideItem->intervals().at(t));

                    if ((current != NULL) && current->selected) {
                        ++high;
                        if (p1->secs+ride->recIntSecs() > current->start && p1->secs< current->stop) {
                            if (mergeIntervals())
#if Q_CC_MSVC
                                dataSetInterval[0].insert(std::make_pair(aepf, cpv));
#else
                                dataSetInterval[0].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                            else
#if Q_CC_MSVC
                                dataSetInterval[high].insert(std::make_pair(aepf, cpv));
#else
                                dataSetInterval[high].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                        }
                    }
                }
                tot_cad_points++;
            }
        }

        if (tot_cad_points > 0) {

           // Now that we have the set of points, transform them into the
           // QwtArrays needed to set the curve's data.
           QVector<QVector<double> > aepfArrayInterval(num_intervals);
           QVector<QVector<double> > cpvArrayInterval(num_intervals);

           for (int i=0;i<num_intervals;i++) {
               std::set<std::pair<double, double> >::const_iterator l(dataSetInterval[i].begin());
               while (l != dataSetInterval[i].end()) {
                   const std::pair<double, double>& dataPoint = *l;

                   aepfArrayInterval[i].push_back(dataPoint.first);
                   cpvArrayInterval[i].push_back(dataPoint.second);

                   ++l;
               }
           }



           // ensure same colors are used for each interval selected
           int num_intervals_defined=0;
           QVector<int> intervalmap;

            num_intervals_defined = rideItem->intervals().count();

            for (int g=0; g<rideItem->intervals().count(); g++) {
                IntervalItem *curr = dynamic_cast<IntervalItem *>(rideItem->intervals().at(g));
                if (curr->selected) intervalmap.append(g);
            }

            // honor display sequencing
            QMap<int, int> intervalOrder;
            int count=0;

            if (mergeIntervals()) intervalOrder.insert(1,0);
            else {
                for (int i=0; i<rideItem->intervals().count(); i++) {

                    IntervalItem *current = dynamic_cast<IntervalItem *>(rideItem->intervals().at(i));

                    if (current != NULL && current->selected == true) {
                        intervalOrder.insert(current->displaySequence, count++);
                    }
                }
            }

            QMapIterator<int, int> order(intervalOrder);
            while (order.hasNext()) {
                order.next();
                int z = order.value();

                QwtPlotCurve *curve;
                curve = new QwtPlotCurve();

                QColor intervalColor;
                if (mergeIntervals())
                    intervalColor = Qt::red;
                else
                    intervalColor.setHsv((intervalmap.count() > 0 ? intervalmap.at(z) : 1) * 255/num_intervals_defined, 255,255);

                QwtSymbol *sym = new QwtSymbol;
                sym->setStyle(QwtSymbol::Ellipse);
                sym->setSize(4*dpiXFactor);
                sym->setBrush(QBrush(Qt::NoBrush));

                QPen pen;
                pen.setColor(intervalColor);
                sym->setPen(pen);
                intervalColor.setAlpha(128);
                sym->setBrush(QBrush(intervalColor));

                curve->setSymbol(sym);
                curve->setStyle(QwtPlotCurve::Dots);
                curve->setRenderHint(QwtPlotItem::RenderAntialiased);
                curve->setSamples(cpvArrayInterval[z],aepfArrayInterval[z]);
                curve->attach(this);

                intervalCurves.append(curve);
            }
        }
    }
    refreshIntervalMarkers();
    replot();
}

void
PfPvPlot::recalcCompare()
{
    // do all the calculations for compare mode
    // we do as a separate method to 'isolate' compare mode
    // updates to ensure we don't break all the charts !!
    maxAEPF = 600;
    maxCPV = 3;

    foreach(CompareInterval ci, context->compareIntervals) {

        if (!ci.isChecked()) continue;

        // calculate maximums
        foreach(const RideFilePoint *p1, ci.data->dataPoints()) {

            if (p1->watts != 0 && p1->cad != 0) {

                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                if (aepf < 255 && aepf > maxAEPF) maxAEPF = aepf;
                if (cpv > maxCPV) maxCPV = cpv;
            }
        }
    }

    if (maxAEPF > 600) {

        setAxisScale(QwtAxis::YLeft, 0, (maxAEPF < 2500) ? (maxAEPF * 1.1) : 2500); // a bit of headroom
        tiqMarker[0]->setYValue(maxAEPF);
        tiqMarker[1]->setYValue(maxAEPF);

    } else {

        maxAEPF = 600; // for background shading and CP curve
        setAxisScale(QwtAxis::YLeft, 0, 600);
        tiqMarker[0]->setYValue(580);
        tiqMarker[1]->setYValue(580);
    }

    if (maxCPV > 3) {

        // round *UP* to next integer for axis to fill nicely
        maxCPV = round(maxCPV + 0.5);
        setAxisScale(QwtAxis::XBottom, 0, maxCPV);
        tiqMarker[0]->setXValue(maxCPV - 0.5);
        tiqMarker[3]->setXValue(maxCPV - 0.5);

    } else {

        maxCPV = 3; // for background shading and CP curve
        setAxisScale(QwtAxis::XBottom, 0, 3);
        tiqMarker[0]->setXValue(2.9);
        tiqMarker[3]->setXValue(2.9);
    }

    // initialize x values used for contours
    contour_xvalues.clear();
    for (double x = 0; x <= maxCPV; x += x / 20 + 0.02) contour_xvalues.append(x);
    contour_xvalues.append(maxCPV);

    double cpv = (cad_ * cl_ * 2.0 * PI) / 60.0;
    mX->setXValue(cpv);

    double aepf = (cp_ * 60.0) / (cad_ * cl_ * 2.0 * PI);
    mY->setYValue(aepf);

    // reset to zero in  case there are none
    timeInQuadrant[0]=
    timeInQuadrant[1]=
    timeInQuadrant[2]=
    timeInQuadrant[3]= 0.0;

    // watch out for null rides
    foreach(CompareInterval ci, context->compareIntervals) {

        if (!ci.isChecked()) continue;

        foreach(const RideFilePoint *p1, ci.data->dataPoints()) {
            if (p1->watts != 0 && p1->cad != 0) {

                double aepf_ = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv_ = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                // classic QA quadrants I II III and IV
                if (aepf_ > aepf && cpv_ > cpv) timeInQuadrant[0] += ci.data->recIntSecs();
                else if (aepf_ > aepf && cpv_ <= cpv) timeInQuadrant[1] += ci.data->recIntSecs();
                else if (aepf_ <= aepf && cpv_ <= cpv) timeInQuadrant[2] += ci.data->recIntSecs();
                else if (aepf_ <= aepf && cpv_ > cpv) timeInQuadrant[3] += ci.data->recIntSecs();

            }
        }
    }

    double totaltime = timeInQuadrant[0] + timeInQuadrant[1] + timeInQuadrant[2] + timeInQuadrant[3] ;

    if (totaltime) {

        QwtText t0(QString("%1%").arg(timeInQuadrant[0] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
        t0.setColor(GColor(CPLOTMARKER));
        tiqMarker[0]->setLabel(t0);

        QwtText t1(QString("%1%").arg(timeInQuadrant[1] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
        t1.setColor(GColor(CPLOTMARKER));
        tiqMarker[1]->setLabel(t1);

        QwtText t2(QString("%1%").arg(timeInQuadrant[2] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
        t2.setColor(GColor(CPLOTMARKER));
        tiqMarker[2]->setLabel(t2);

        QwtText t3(QString("%1%").arg(timeInQuadrant[3] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
        t3.setColor(GColor(CPLOTMARKER));
        tiqMarker[3]->setLabel(t3);

    } else {

        tiqMarker[0]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[1]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[2]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[3]->setLabel(QwtText("",QwtText::PlainText));

    }
}

void
PfPvPlot::recalc()
{
    // we're for a ride only..
    if (context->isCompareIntervals) {
        recalcCompare();
        return;
    }

    // adjust the scales if we have some big values
    // this can happen with track sprinters who put
    // out big numbers for power and cadence since
    // hey have a fixed gear and big quads!
    maxAEPF = 600;
    maxCPV = 3;

    RideFile *ride;
    if (rideItem && (ride=rideItem->ride())) {

        // calculate maximums
        foreach(const RideFilePoint *p1, ride->dataPoints()) {

            if (p1->watts != 0 && p1->cad != 0) {

                double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                if (aepf < 2500 && aepf > maxAEPF) maxAEPF = aepf;
                if (cpv > maxCPV) maxCPV = cpv;
            }
        }
    }

    if (maxAEPF > 600) {

        setAxisScale(QwtAxis::YLeft, 0, (maxAEPF < 2500) ? (maxAEPF * 1.1) : 2500); // a bit of headroom
        tiqMarker[0]->setYValue(maxAEPF);
        tiqMarker[1]->setYValue(maxAEPF);

    } else {

        maxAEPF = 600; // for background shading and CP curve
        setAxisScale(QwtAxis::YLeft, 0, 600);
        tiqMarker[0]->setYValue(580);
        tiqMarker[1]->setYValue(580);
    }

    if (maxCPV > 3) {

        // round *UP* to next integer for axis to fill nicely
        maxCPV = round(maxCPV + 0.5);
        setAxisScale(QwtAxis::XBottom, 0, maxCPV);
        tiqMarker[0]->setXValue(maxCPV - 0.5);
        tiqMarker[3]->setXValue(maxCPV - 0.5);

    } else {

        maxCPV = 3; // for background shading and CP curve
        setAxisScale(QwtAxis::XBottom, 0, 3);
        tiqMarker[0]->setXValue(2.9);
        tiqMarker[3]->setXValue(2.9);
    }

    // initialize x values used for contours
    contour_xvalues.clear();
    for (double x = 0; x <= maxCPV; x += x / 20 + 0.02) contour_xvalues.append(x);
    contour_xvalues.append(maxCPV);

    double cpv = (cad_ * cl_ * 2.0 * PI) / 60.0;
    mX->setXValue(cpv);

    double aepf = (cp_ * 60.0) / (cad_ * cl_ * 2.0 * PI);
    mY->setYValue(aepf);

    // watch out for null rides
    if (rideItem && (ride=rideItem->ride())) {

        timeInQuadrant[0]=
        timeInQuadrant[1]=
        timeInQuadrant[2]=
        timeInQuadrant[3]= 0.0;

        foreach(const RideFilePoint *p1, ride->dataPoints()) {
            if (p1->watts != 0 && p1->cad != 0) {

                double aepf_ = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                double cpv_ = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                // classic QA quadrants I II III and IV
                if (aepf_ > aepf && cpv_ > cpv) timeInQuadrant[0] += ride->recIntSecs();
                else if (aepf_ > aepf && cpv_ <= cpv) timeInQuadrant[1] += ride->recIntSecs();
                else if (aepf_ <= aepf && cpv_ <= cpv) timeInQuadrant[2] += ride->recIntSecs();
                else if (aepf_ <= aepf && cpv_ > cpv) timeInQuadrant[3] += ride->recIntSecs();

            }
        }
        double totaltime = timeInQuadrant[0] + timeInQuadrant[1] + timeInQuadrant[2] + timeInQuadrant[3] ;

        if (totaltime) {

            QwtText t0(QString("%1%").arg(timeInQuadrant[0] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
            t0.setColor(GColor(CPLOTMARKER));
            tiqMarker[0]->setLabel(t0);

            QwtText t1(QString("%1%").arg(timeInQuadrant[1] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
            t1.setColor(GColor(CPLOTMARKER));
            tiqMarker[1]->setLabel(t1);

            QwtText t2(QString("%1%").arg(timeInQuadrant[2] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
            t2.setColor(GColor(CPLOTMARKER));
            tiqMarker[2]->setLabel(t2);

            QwtText t3(QString("%1%").arg(timeInQuadrant[3] / totaltime * 100, 0, 'f', 1),QwtText::PlainText);
            t3.setColor(GColor(CPLOTMARKER));
            tiqMarker[3]->setLabel(t3);

        } else {

            tiqMarker[0]->setLabel(QwtText("",QwtText::PlainText));
            tiqMarker[1]->setLabel(QwtText("",QwtText::PlainText));
            tiqMarker[2]->setLabel(QwtText("",QwtText::PlainText));
            tiqMarker[3]->setLabel(QwtText("",QwtText::PlainText));

        }

    } else {

        tiqMarker[0]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[1]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[2]->setLabel(QwtText("",QwtText::PlainText));
        tiqMarker[3]->setLabel(QwtText("",QwtText::PlainText));

    }

    QVector<double> yvalues(contour_xvalues.size());

    if (cp_) {

        // reinitialise array
        for (int i = 0; i < contour_xvalues.size(); i ++)
            yvalues[i] = (cpv < cp_ / 1e6) ?  1e6 : cp_ / contour_xvalues[i];

        // generate curve at a given power
        cpCurve->setSamples(contour_xvalues, yvalues);

    } else {

        // an empty curve if no power (or zero power) is specified
        QVector<double> data;
        cpCurve->setSamples(data,data);
    }
    if (pmax_) {

        // reinitialise array
        for (int i = 0; i < contour_xvalues.size(); i ++)
            yvalues[i] = (cpv < pmax_ / 1e6) ?  1e6 : pmax_ / contour_xvalues[i];

        // generate curve at a given power
        pmaxCurve->setSamples(contour_xvalues, yvalues);

    } else {

        // an empty curve if no power (or zero power) is specified
        QVector<double> data;
        pmaxCurve->setSamples(data,data);
    }
}

int
PfPvPlot::getCP()
{
    return cp_;
}

void
PfPvPlot::setCP(int cp)
{
    cp_ = cp;
    recalc();
    emit changedCP( QString("%1").arg(cp) );
}

int
PfPvPlot::getCAD()
{
    return cad_;
}

void
PfPvPlot::setCAD(int cadence)
{
    cad_ = cadence;
    recalc();
    emit changedCAD( QString("%1").arg(cadence) );
}

double
PfPvPlot::getCL()
{
    return cl_;
}

void
PfPvPlot::setCL(double cranklen)
{
    cl_ = cranklen;
    recalc();
    emit changedCL( QString("%1").arg(cranklen) );
}

int
PfPvPlot::getPMax()
{
    return pmax_;
}

void
PfPvPlot::setPMax(int pmax)
{
    pmax_ = pmax;
    recalc();
    emit changedPMax( QString("%1").arg(pmax) );
}

// process checkbox for zone shading
void
PfPvPlot::setShadeZones(bool value)
{
    shade_zones = value;

    // if there are defined zones and labels, set their visibility
    for (int i = 0; i < zoneCurves.size(); i ++)
    zoneCurves[i]->setVisible(shade_zones);
    for (int i = 0; i < zoneLabels.size(); i ++)
    zoneLabels[i]->setVisible(shade_zones);

    //replot();
}

void
PfPvPlot::setMergeIntervals(bool value)
{
    merge_intervals = value;
    showIntervals(rideItem);
}

void
PfPvPlot::setFrameIntervals(bool value)
{
    frame_intervals = value;
    showIntervals(rideItem);
}

void
PfPvPlot::setGearRatioDisplay(bool value)
{
    gear_ratio_display = value;
    if (context->isCompareIntervals) return;
    mainCurvesSetVisible(true);
    replot();
}

void
PfPvPlot::showCompareIntervals()
{
    // Remove interval curve
    if (intervalCurves.size()) {
       QListIterator<QwtPlotCurve *> i(intervalCurves);
       while (i.hasNext()) {
           QwtPlotCurve *curve = i.next();
           curve->detach();
           //delete curve;
       }
    }
    intervalCurves.clear();

    // quickly erase old data
    mainCurvesSetVisible(false);

    int num_intervals = context->compareIntervals.count();

    // If not compare mode or no intervals
    if (context->compareIntervals.count() == 0) return;

    recalcCompare();

    QVector<std::set<std::pair<double, double> > > dataSetInterval(num_intervals);
    long tot_cad_points = 0;

    // prepare aggregates
    for (int high=-1, i = 0; i < context->compareIntervals.size(); ++i) {
        CompareInterval interval = context->compareIntervals.at(i);

        if (interval.isChecked())  {
            ++high;

            foreach(const RideFilePoint *p1, interval.data->dataPoints()) {
                if (p1->watts != 0 && p1->cad != 0) {
                    double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
                    double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;

                    if (mergeIntervals())
#if Q_CC_MSVC
                        dataSetInterval[0].insert(std::make_pair(aepf, cpv));
#else
                        dataSetInterval[0].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                    else
#if Q_CC_MSVC
                        dataSetInterval[high].insert(std::make_pair(aepf, cpv));
#else
                        dataSetInterval[high].insert(std::MAKEPAIR<double, double>(aepf, cpv));
#endif
                    tot_cad_points++;
                }
            }
        }

    }

    if (tot_cad_points > 0) {

        // Now that we have the set of points, transform them into the
        // QwtArrays needed to set the curve's data.
        QVector<QVector<double> > aepfArrayInterval(num_intervals);
        QVector<QVector<double> > cpvArrayInterval(num_intervals);

        for (int i=0;i<num_intervals;i++) {
            std::set<std::pair<double, double> >::const_iterator l(dataSetInterval[i].begin());
            while (l != dataSetInterval[i].end()) {
                const std::pair<double, double>& dataPoint = *l;

                aepfArrayInterval[i].push_back(dataPoint.first);
                cpvArrayInterval[i].push_back(dataPoint.second);

                ++l;
            }
        }

        // ensure same colors are used for each interval selected
        QMap<int, CompareInterval> intervalmap;
        int count=0;

        if (num_intervals > 0) {
            for (int g=0; g<num_intervals; g++) {
                CompareInterval interval = context->compareIntervals.at(g);
                if (interval.checked) intervalmap.insert(count++, interval);
            }
        }

        QMapIterator<int, CompareInterval> order(intervalmap);
        while (order.hasNext()) {
            order.next();
            CompareInterval interval = order.value();

            QwtPlotCurve *_curve;
            _curve = new QwtPlotCurve();

            QColor intervalColor;
            if (mergeIntervals())
                intervalColor = Qt::red;
            else
                intervalColor = interval.color;

            QwtSymbol *sym = new QwtSymbol;
            sym->setStyle(QwtSymbol::Ellipse);
            sym->setSize(4*dpiXFactor);
            sym->setBrush(QBrush(Qt::NoBrush));

            QPen pen;
            pen.setColor(intervalColor);
            sym->setPen(pen);
            intervalColor.setAlpha(128);
            sym->setBrush(QBrush(intervalColor));

            _curve->setSymbol(sym);
            _curve->setStyle(QwtPlotCurve::Dots);
            _curve->setRenderHint(QwtPlotItem::RenderAntialiased);
            _curve->setSamples(cpvArrayInterval[order.key()],aepfArrayInterval[order.key()]);
            _curve->attach(this);

            intervalCurves.append(_curve);
        }
    }

    refreshZoneItems();
    replot();
}

void
PfPvPlot::mainCurvesSetVisible(bool visible) {

    if (gearRatioCurves.size()) {
        QListIterator<QwtPlotCurve *> i(gearRatioCurves);
        while (i.hasNext()) {
            QwtPlotCurve *curve = i.next();
            curve->setVisible(visible ? gear_ratio_display : false);
        }
    }

    curve->setVisible(visible ? !gear_ratio_display : false);

}
