diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index cb2b292bb..2a3538c40 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -29,6 +29,7 @@ #include "DownloadRideDialog.h" #include "ManualRideDialog.h" #include "HistogramWindow.h" +#include "ModelWindow.h" #include "RealtimeWindow.h" #include "RideItem.h" #include "IntervalItem.h" @@ -278,6 +279,13 @@ MainWindow::MainWindow(const QDir &home) : pfPvWindow = new PfPvWindow(this); tabWidget->addTab(pfPvWindow, tr("PF/PV Plot")); + //////////////////////// 3d Model Window //////////////////////////// + +#ifdef GC_HAVE_QWTPLOT3D + modelWindow = new ModelWindow(this, home); + tabWidget->addTab(modelWindow, tr("3D")); +#endif + //////////////////////// Weekly Summary //////////////////////// // add daily distance / duration graph: @@ -1175,7 +1183,7 @@ void MainWindow::tabChanged(int index) { criticalPowerWindow->setActive(index == 2); - performanceManagerWindow->setActive(index == 6); + performanceManagerWindow->setActive(tabWidget->widget(index) == performanceManagerWindow); } void diff --git a/src/MainWindow.h b/src/MainWindow.h index 12ae5fc49..e16bba61d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -35,6 +35,7 @@ class PfPvWindow; class QwtPlotPanner; class QwtPlotPicker; class QwtPlotZoomer; +class ModelWindow; class RealtimeWindow; class RideFile; class WeeklySummaryWindow; @@ -156,6 +157,7 @@ class MainWindow : public QMainWindow HistogramWindow *histogramWindow; WeeklySummaryWindow *weeklySummaryWindow; CriticalPowerWindow *criticalPowerWindow; + ModelWindow *modelWindow; QTreeWidgetItem *allRides; QTreeWidgetItem *allIntervals; QSplitter *leftLayout; diff --git a/src/ModelPlot.cpp b/src/ModelPlot.cpp new file mode 100644 index 000000000..cb3137743 --- /dev/null +++ b/src/ModelPlot.cpp @@ -0,0 +1,1309 @@ +/* + * 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 "ModelPlot.h" +#include "ModelWindow.h" +#include "MainWindow.h" +#include "Zones.h" +#include "RideFile.h" +#include "Units.h" // for MILES_PER_KM + +#include + +using namespace Qwt3D; // namespace ref is only visible in this file (is not in any headers) + +/*---------------------------------------------------------------------- + * MODEL DATA PROVIDER + * + * This function is called by the surface plot to get z values for an + * xy pair. The data is set by a call from ModelPlot (from ModelWindow) + * when a user either selects a new ridefile or changes the x/y/z/color + * comboboxes. + * + * setData - sets the model data from a ridefile using the axis passed + * and returns the mesh values as an xy to set the surface + * plot. Will clear existing data first. + * + * operator () - called by the surface plot to set z for a given x and y + * + *----------------------------------------------------------------------*/ + +// util function to create an x/y key for QHash, effective and quick enough +static QString xystring(double x, double y) { return QString("%1:%2").arg((int)x).arg((int)y); } +#if 0 +static void unxystring(QString val, double &x, double &y) +{ + QRegExp it("^([^:]*):([^:]*)$"); + it.exactMatch(val); + x = it.cap(1).toDouble(); + y = it.cap(2).toDouble(); +} +#endif + +// returns the color for an xyz point +class ModelDataColor : public Color +{ + // return RGBA color for x,y,z + Qwt3D::RGBA operator () (double x, double y, double) const + { + QColor cHSV, cRGB; + double val = color.value(xystring(x,y), 0.0); + RGBA colour; + if (!val) { + return RGBA(255,255,255,0); // see thru + } if (iszones == true) { // zone 0 is set to zone 1 to distinguish from no value + cHSV = zonecolor.value(val-1, QColor(0,0,0)); + } else if (val) { + cHSV.setHsv((double)255 * (double)((val-min)/(max-min)), 255, 255); + } + cRGB = cHSV.convertTo(QColor::Rgb); + colour.r = (double) cRGB.red()/(double)255; + colour.g = (double) cRGB.green()/(double)255; + colour.b = (double) cRGB.blue()/(double)255; + colour.a = 1.0; + return colour; + } + + Qwt3D::ColorVector &createVector(Qwt3D::ColorVector &vec) + { + RGBA colour; + QColor cHSV, cRGB; + Qwt3D::ColorVector::iterator it; + + // clear current + vec.clear(); + + // update the color vector to use our color scheme + if (iszones) { + for (int i=0; i< 7; i++) { + cRGB = zonecolor.value(i, QColor(0,0,0)).convertTo(QColor::Rgb); + colour.r = (double) cRGB.red()/(double)255; + colour.g = (double) cRGB.green()/(double)255; + colour.b = (double) cRGB.blue()/(double)255; + colour.a = 1.0; + + it = vec.end(); + vec.insert(it, 1, colour); + } + } else { + double val; + int i; + // just add 100 colors graded from min to max + for (i=0, val=min; i<100; i++, val += ((max-min)/100)) { + + cHSV.setHsv((double)255 * (double)((val-min)/(max-min)), 255, 255); + cRGB = cHSV.convertTo(QColor::Rgb); + colour.r = (double) cRGB.red()/(double)255; + colour.g = (double) cRGB.green()/(double)255; + colour.b = (double) cRGB.blue()/(double)255; + colour.a = 1.0; + it = vec.end(); + vec.insert(it, 1, colour); + } + } + return vec; + } + + public: + QHash color; + QHash num; // xy map with count of values for averaging + double min, max; + + bool iszones; // if the color value is a zone number + QMap zonecolor; + +}; + + +// Sorry Sean et al. These local statics are a nasty'ism I'll +// look to tidy later. I cannot reference a class for these +// since qwtplot3d manages its data providers internally with clones +// and when it calls an enricher the data provider is not made +// available. +// +// Ideally they would be held within BasicModelPlot::dataModelProvider +// and were until I needed to implement a custom enrichment to +// support interval selection/display +// +// As a result I use these to allow the DataModelProvider to +// share data with the Bar enrichment. I also tried to use a ref +// to the dataModelProvider but it caused a nasty SEGV +// +// Lets just turn a blind eye ;-) +// + +#define SHOW_INTERVALS 1 +#define SHOW_FRAME 2 + +static double diag_; +static int intervals_; // SHOW_INTERVALS | SHOW_MAX +static double zpane=0; +static QHash iz; // for selected intervals +static QHash inum; // for selected intervals + +class ModelDataProvider : public Function +{ + + public: + + ModelDataProvider (BasicModelPlot &plot, ModelSettings *settings); + void setData(RideFile *ride, int x, int y, int z, int col); // set the maps and return mesh dimension + + // return z value for x,y - std qwt3plot method + double operator () (double x, double y) + { + // return the z value for x and y + return mz.value(xystring(x,y), 0.0); + } + double intervals (double x, double y) // return value for selected intervals + { + return iz.value(xystring(x,y), 0.0)-minz; + } + double getMinz() { return minz; } + double getMaxz() { return maxz; } + + ~ModelDataProvider() + { + mz.clear(); + mnum.clear(); + iz.clear(); + inum.clear(); + } + + private: + + QHash mz; // xy map with max z values; + QHash mnum; // xy map with count of values for averaging + + + double pointType(const RideFilePoint *, int); + QString describeType(int, bool); + double maxz, minz; +}; + +double +ModelDataProvider::pointType(const RideFilePoint *point, int type) +{ + // 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 : return point->kph; + case MODEL_ALT : return point->alt; + case MODEL_PEDALFORCE : return point->nm; + case MODEL_TIME : return point->secs; + case MODEL_DISTANCE : return point->km; + case MODEL_INTERVAL : return point->interval; + case MODEL_LAT : return point->lat; + case MODEL_LONG : return point->lon; + + // 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 +ModelDataProvider::describeType(int type, bool longer) +{ + // 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 : return ("Speed (kph)"); //XXX metric / imperial! + case MODEL_ALT : return ("Altitude (meters)"); // XXX metric / imperial + case MODEL_PEDALFORCE : return ("Pedal Force (nm)"); + case MODEL_TIME : return ("Elapsed Time (secs)"); + case MODEL_DISTANCE : return ("Elapsed Distance (km)"); //XXX metric/imperial + case MODEL_INTERVAL : return ("Interval Number"); // XXX implemented differently + case MODEL_LAT : return ("Latitude (degree offset)"); + case MODEL_LONG : return ("Longitude (degree offset)"); + + // 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_PEDALFORCE : 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"); + } + return ("None");; // ? unknown channel ? + } +} + +/*---------------------------------------------------------------------- + * Setup the data model and plot according to the settings passed from + * mainwindow. + * + * Truthfully, this is where all the hard work is done! + * Edit it with caution, there be dragons here. + * + *----------------------------------------------------------------------*/ +ModelDataProvider::ModelDataProvider (BasicModelPlot &plot, ModelSettings *settings) : Function(plot) +{ + // if there are no settings or incomplete settings + // create a null data plot + if (settings == NULL || settings->ride == NULL || + settings->x == 0 || settings->y == 0 || settings->z == 0) { + // initialise a null plot + setDomain(0,0,0,0); + setMinZ(0); + create(); + + return; + } + + // Run through the ridefile points putting the selected + // values into the approprate bins + settings->colorProvider->color.clear(); + settings->colorProvider->num.clear(); + settings->colorProvider->zonecolor.clear(); + + //plot.makeCurrent(); + + double maxbinx =0, maxbiny =0; + double minbinx =65535, minbiny =65535; + double mincol =65535, maxcol =0; + + // + // Create Plot dataset, filter on values and calculate averages etc + // + foreach(const RideFilePoint *point, settings->ride->ride()->dataPoints()) { + + // get x and z bin values - round to nearest bin + double dx = pointType(point, settings->x)/settings->xbin; + int binx = settings->xbin * Qwt3D::round(dx); + + double dy = pointType(point, settings->y)/settings->ybin; + int biny = settings->ybin * Qwt3D::round(dy); + + // ignore zero points + if (settings->ignore && (binx==0 || biny==0)) continue; + + // get z value + double zed=0; + if (settings->z == MODEL_XYTIME) zed = settings->ride->ride()->recIntSecs(); // time at + else zed = pointType(point, settings->z); // raw data + + // get color value + double color=0; + if (settings->color == MODEL_XYTIME) color = settings->ride->ride()->recIntSecs(); // time at + else color = pointType(point, settings->color); // raw data + + // min max + if (color > maxcol) maxcol = color; + if (color < mincol) mincol = color; + if (binx > maxbinx) maxbinx = binx; + if (binx < minbinx) minbinx = binx; + if (biny > maxbiny) maxbiny = biny; + if (biny < minbiny) minbiny = biny; + + // How many & Curent Value + // ZED + QString lookup = xystring(binx,biny); + int count = mnum.value(lookup, 0.0); + double currentz = mz.value(lookup, 0.0); + + if (settings->z == MODEL_XYTIME) { + count++; + zed += currentz; + } else { + if (count) zed = ((currentz*count)+zed)/(count+1); // average + } + + // always update the max values + mz.insert(lookup, zed); + mnum.insert(lookup, count); + + // NO INTERVALS COLOR IS FOR ALL SAMPLES + if (settings->intervals.count() == 0 ) { + intervals_ = 0; + + int colcount = settings->colorProvider->num.value(lookup, 0.0); + double currentcol = settings->colorProvider->color.value(lookup,0.0); + + if (settings->color == MODEL_XYTIME) { // color in time + colcount++; + color += currentcol; + } else { // color in average of + if (colcount) color = ((currentcol*colcount)+color)/(colcount+1); // average + } + settings->colorProvider->color.insert(lookup, color); + settings->colorProvider->num.insert(lookup, colcount); + } + + // WE HAVE INTERVALS! COLOR AND INTERVAL Z VALUES NEED TO BE TREATED + // DIFFERENTLY NOW - COLOR IS FOR SELECTED INTERVALS AND WE MAINTAIN + // A SECOND SET OF Z VALUES SO WE HAVE MAX + INTERVALS + if (settings->intervals.count() > 0) { + intervals_ = SHOW_INTERVALS; + if (settings->frame == true) intervals_ |= SHOW_FRAME; + + QString lookup = xystring(binx, biny); // XXX SEGV on QString in multiple hashes!!!! XXX + + // filter for interval + for(int i=0; iintervals.count(); i++) { + IntervalItem *curr = settings->intervals.at(i); + if (point->secs >= curr->start && point->secs <= curr->stop) { + + // update colors + int colcount = settings->colorProvider->num.value(lookup,0.0); + double currentcol = settings->colorProvider->color.value(lookup, 0.0); + + if (settings->color == MODEL_XYTIME) { // color in time + colcount++; + color += currentcol; + } else { // color in average of + if (colcount) color = ((currentcol*colcount)+color)/(colcount+1); // average + } + settings->colorProvider->color.insert(lookup, color); + settings->colorProvider->num.insert(lookup, colcount); + + double ized=0; + if (settings->z == MODEL_XYTIME) ized = settings->ride->ride()->recIntSecs(); // time at + else ized = pointType(point, settings->z); // raw data + // update interval values + int count = inum.value(lookup, 0.0); + double currentz = iz.value(lookup, 0.0); + + if (settings->z == MODEL_XYTIME) { + count++; + ized += currentz; + } else { + if (count) ized = ((currentz*count)+ized)/(count+1); // average + } + + iz.insert(lookup, ized); + inum.insert(lookup, count); + break; + } + } + } + } + + if (mz.count() == 0) { + + // create a null plot -- bin too large! + plot.setTitle("No data or bin size too large"); + setDomain(0,0,0,0); + setMesh(2,2); + setMinZ(0); + mz.clear(); + settings->colorProvider->color.clear(); + create(); + return; + } + + // POST PROCESS DATA SET + + // COLOR + // if needed convert average power to the related power zone + // but only if ranges are defined i.e. user has set CP + const Zones *zones = settings->ride->zones; + int zone_range = settings->ride->zoneRange(); + if (settings->color == MODEL_POWERZONE && zone_range >= 0) { + // if we need to color by power zones + // zones are setup and we want to color by them + maxcol = settings->colorProvider->max = zones->numZones(zone_range); + mincol = settings->colorProvider->min = 1; + + // add the zone colors to the color map + for (int i=0; icolorProvider->zonecolor.insert + (i, zoneColor(i, zones->numZones(zone_range))); + } + + // iterate over the existing power values converting to a power zone + QHashIterator coli(settings->colorProvider->color); + while (coli.hasNext()) { + coli.next(); + QString lookup = coli.key(); + double color = coli.value(); + // turn into power zone + color = zones->whichZone(zone_range, color); + // overwrite existing power average with zone number + // BUT! the zone numbers start at 1 not 0 here to distinguish + // between zone 0 and no value at all + settings->colorProvider->color.insert(lookup, color+1); + + } + settings->colorProvider->iszones = true; + } else if (settings->color == MODEL_NONE) { + settings->colorProvider->iszones = false; + settings->colorProvider->color.clear(); + } else { + // otherwise just turn off zoning + settings->colorProvider->iszones = false; + } + + // TIME + // + // Convert from absolute values to %age of the entire ride + + double duration = settings->ride->ride()->dataPoints().last()->secs + + settings->ride->ride()->recIntSecs(); + + // Multis... + if (settings->z == MODEL_XYTIME) { + // time on Z axis + QHashIterator zi(mz); + while (duration && settings->z == MODEL_XYTIME && zi.hasNext()) { + zi.next(); + double timePercent = (zi.value()/duration) * 100; + mz.insert(zi.key(), timePercent); + } + } + // Intervals + if (settings->z == MODEL_XYTIME) { + // time on Z axis + QHashIterator ii(iz); + while (duration && settings->z == MODEL_XYTIME && ii.hasNext()) { + ii.next(); + double timePercent = (ii.value()/duration) * 100; + iz.insert(ii.key(), timePercent); + } + } + + // time on Color + if (settings->color == MODEL_XYTIME) { + mincol=65535; maxcol=0; + QHashIterator ci(settings->colorProvider->color); + while (duration && settings->color == MODEL_XYTIME && ci.hasNext()) { + ci.next(); + double timePercent = (ci.value()/duration) * 100; + if (timePercent > maxcol) maxcol = timePercent; + if (timePercent < mincol) mincol = timePercent; + settings->colorProvider->color.insert(ci.key(), timePercent); + } + } + + // MIN and MAX Z (impacts chart geometry) + // We DO NOT do the same for color since they represent + // the entire data set and not just the intervals selected (if any) + bool first = true; + QHashIterator iz(mz); + while (iz.hasNext()) { + double z; + iz.next(); + + z = iz.value(); + + if (first == true) { + minz = maxz = iz.value(); + } else { + if (z > maxz) maxz = z; + if (z < minz) minz = z; + } + first = false; + } + + settings->colorProvider->min = mincol; + settings->colorProvider->max = maxcol; + + // + // Adjust the plot settings to reflect the data set + // + setMinZ(minz); + setMaxZ(maxz); + + QFont font; // has all the defaults in it + + // + // Setup the color legend + // + if (settings->legend == true && settings->color != MODEL_NONE) { + plot.showColorLegend(true); + plot.legend()->setTitleFont(font.family(), 8, QFont::Normal); + plot.legend()->setOrientation(Qwt3D::ColorLegend::BottomTop, Qwt3D::ColorLegend::Left); + plot.legend()->setLimits(mincol, maxcol); + if (settings->colorProvider->iszones == true) { + plot.legend()->setMajors(maxcol); + plot.legend()->setMinors(0); + } else { + plot.legend()->setMajors(10); + plot.legend()->setMinors(0); + } + plot.legend()->setTitleString(describeType(settings->color, false)); + } else { + plot.showColorLegend(false); + } + + // mesh size + int mx = (maxbinx-minbinx) / settings->xbin; + int my = (maxbiny-minbiny) / settings->ybin; + + // mesh MUST be at least 2x2 including 0,0 + while (mx < 2) { + maxbinx += settings->xbin; + mx++; + } + + while(my < 2) { + maxbiny += settings->ybin; + my++; + } + + // add some graph paper so the plot is pretty + if (mx < 4) { + minbinx -= settings->xbin; + mx++; + } + + if (my < 4) { + minbiny -= settings->ybin; + my++; + } + + // mesh is number of bins PLUS ONE (bug in qwtplot3d) + setMesh(mx + 1, my + 1); + + // domain is max values in each axis + setDomain(maxbinx,minbinx,minbiny,maxbiny); // max x/y vals + + // set the barsize to a sensible radius (20% space) + // the +settting->xbin bit is to offset the additional mx/my bug + double xr = 0.8 * (((double)settings->xbin/((double)(maxbinx-minbinx)+(settings->xbin))) / 2.0); + double yr = 0.8 * (((double)settings->ybin/((double)(maxbiny-minbiny)+(settings->ybin))) / 2.0); + + diag_ = xr < yr ? (xr*(maxbinx-minbinx)) : (yr*(maxbiny-minbiny)); + + // now update the model - before the chart axis/legends + create(); + + double xscale, yscale, zscale; + + if ((maxbinx-minbinx) >= (maxbiny-minbiny) && (maxbinx-minbinx) >= (maxz-minz)) { + // scale is set off the x-axis + xscale = 1; + yscale = (maxbinx-minbinx)/(maxbiny-minbiny); + zscale = (maxbinx-minbinx)/(maxz-minz); + } else if ((maxbiny-minbiny) >= (maxbinx-minbinx) && (maxbiny >= minbiny) >= (maxz-minz)) { + // scale is set off the y-axis + xscale = (maxbiny-minbiny)/(maxbinx-minbinx); + yscale = 1; + zscale = (maxbiny-minbiny)/(maxz-minz); + } else { + // scale is set off the z-axis + xscale = (maxz-minz)/(maxbinx-minbinx); + yscale = (maxz-minz)/(maxbiny-minbiny); + zscale = 1; + } + + // must be integers!? + if (xscale < 1) { + double factor = 1 / xscale; + xscale = 1; + yscale *= factor; + zscale *= factor; + } + if (yscale < 1) { + double factor = 1/ yscale; + yscale = 1; + xscale *= factor; + zscale *= factor; + } + if (zscale < 1) { + double factor = 1/ zscale; + zscale = 1; + yscale *= factor; + xscale *=factor; + } + + plot.setScale(xscale, yscale, zscale); + plot.setTitle(""); + plot.setCoordinateStyle(FRAME); + plot.setMeshLineWidth(1); + plot.coordinates()->setLineWidth(1); + plot.coordinates()->setNumberFont(font.family(),font.pointSize()); + plot.setTitleFont(font.family(),font.pointSize(), QFont::Bold); + + plot.coordinates()->setLabelFont(font.family(), font.pointSize(), QFont::Bold); + plot.coordinates()->axes[Z1].setLabelString(describeType(settings->z, true)); + plot.coordinates()->axes[Z2].setLabelString(describeType(settings->z, true)); + plot.coordinates()->axes[Z3].setLabelString(describeType(settings->z, true)); + plot.coordinates()->axes[Z4].setLabelString(describeType(settings->z, true)); + plot.coordinates()->axes[X1].setLabelString(describeType(settings->x, true)); + plot.coordinates()->axes[X2].setLabelString(describeType(settings->x, true)); + plot.coordinates()->axes[X3].setLabelString(describeType(settings->x, true)); + plot.coordinates()->axes[X4].setLabelString(describeType(settings->x, true)); + plot.coordinates()->axes[Y1].setLabelString(describeType(settings->y, true)); + plot.coordinates()->axes[Y2].setLabelString(describeType(settings->y, true)); + plot.coordinates()->axes[Y3].setLabelString(describeType(settings->y, true)); + plot.coordinates()->axes[Y4].setLabelString(describeType(settings->y, true)); + plot.coordinates()->axes[Z1].draw(); + plot.coordinates()->axes[Z2].draw(); + plot.coordinates()->axes[Z3].draw(); + plot.coordinates()->axes[X1].draw(); + plot.coordinates()->axes[X2].draw(); + plot.coordinates()->axes[X3].draw(); + plot.coordinates()->axes[Y1].draw(); + plot.coordinates()->axes[Y2].draw(); + plot.coordinates()->axes[Y3].draw(); + + for (unsigned int i=0; i < plot.coordinates()->axes.size(); i++) { + plot.coordinates()->axes[i].setMajors(7); + plot.coordinates()->axes[i].setMinors(5); + } + plot.setIsolines(10); + plot.setSmoothMesh(true); + plot.coordinates()->adjustLabels(diag_*2); + if (settings->gridlines == true) + plot.coordinates()->setGridLines(true, true, Qwt3D::BACK | Qwt3D::LEFT | Qwt3D::FLOOR); + else + plot.coordinates()->setGridLines(true, true, 0); + + // turn off zpane -- causes nasty flashing when left on between plots + zpane = 0; + +} + +/*---------------------------------------------------------------------- + * BASIC MODEL PLOT + * This is the qwt3d plot object + * + * Constructor - initialises an empty plot + * setData - calls the model data provider to setup data + * and then replots via updateData, updateGL. + * + *----------------------------------------------------------------------*/ + +BasicModelPlot::BasicModelPlot(MainWindow *parent, ModelSettings *settings) : main(parent) +{ + diag_=0; + currentStyle = STYLE_BAR; + + // the color provider returns a color for an xyz + modelDataColor = new ModelDataColor; + if (settings) settings->colorProvider = modelDataColor; + + // the data provider returns a z for an x,y + modelDataProvider = new ModelDataProvider(*this, settings); + setDataColor(modelDataColor); + + // box style x/y/z + setCoordinateStyle(FRAME); + + // start with bar chart + bar = (Bar *) this->setPlotStyle(Bar()); + + //coordinates()->setAutoScale(true); + + // add grid lines and antialias them for smoother lines + //coordinates()->setGridLines(true, true); + //coordinates()->setLineSmooth(true); + //setSmoothMesh(true); + + // some detailed axes points + for (unsigned int i=0; i < coordinates()->axes.size(); i++) { + coordinates()->axes[i].setMajors(7); + coordinates()->axes[i].setMinors(5); + } + setMeshLineWidth(1); + coordinates()->setGridLinesColor(RGBA(0,0,0.5)); + coordinates()->setLineWidth(1); + + // put some space between the axes tic labels and the plot + // to make it easier to read + coordinates()->adjustNumbers(25); + + // orthogonal view + setOrtho(false); + + // no lighting it makes it tricky to read + // when there are LOTS of bars + blowout(); + + // set shift zoom etc + resetViewPoint(); + + updateData(); + updateGL(); +} + +void +BasicModelPlot::setStyle(int index) +{ + if (currentStyle == STYLE_BAR) degrade(bar); + else degrade(water); + + switch (index) { + + case 0 : // BAR + bar = (Bar*)this->setPlotStyle(Bar()); + showNormals(false); + updateNormals(); + currentStyle = STYLE_BAR; + break; + case 1 : // SURFACE GRID + setPlotStyle(FILLEDMESH); + water = (Water *)addEnrichment(Water()); + showNormals(false); + updateNormals(); + currentStyle = STYLE_GRID; + break; + case 2 : // SURFACE SMOOTH + setPlotStyle(FILLED); + water = (Water *)addEnrichment(Water()); + showNormals(false); + updateNormals(); + currentStyle = STYLE_SURFACE; + break; + case 3 : // DOTS + setPlotStyle(Qwt3D::POINTS); + water = (Water *)addEnrichment(Water()); + showNormals(true); + updateNormals(); + currentStyle = STYLE_DOTS; + break; + } + updateData(); + updateGL(); +} + +void +BasicModelPlot::setData(ModelSettings *settings) +{ + delete modelDataProvider; + settings->colorProvider = modelDataColor; + modelDataProvider = new ModelDataProvider(*this, settings); + //modelDataProvider->assign(this); + //create(); + //resetViewPoint(); + //updateNormals(); + updateData(); + updateGL(); +} + +void +BasicModelPlot::setFrame(bool frame) +{ + if (intervals_ && frame == true) { + intervals_ |= SHOW_FRAME; + } else if (frame== false) { + intervals_ &= ~SHOW_FRAME; + } + updateData(); + updateGL(); +} + +void +BasicModelPlot::setLegend(bool legend, int coltype) +{ + if (legend == true && coltype != MODEL_NONE) { + showColorLegend(true); + } else { + showColorLegend(false); + } +} + +void +BasicModelPlot::setGrid(bool grid) +{ + if (grid == true) + coordinates()->setGridLines(true, true, Qwt3D::BACK | Qwt3D::LEFT | Qwt3D::FLOOR); + else + coordinates()->setGridLines(true, true, 0); + updateData(); + updateGL(); +} + +void +BasicModelPlot::setZPane(int z) +{ + //zpane = (modelDataProvider->maxz-modelDataProvider->minz) / 100 * z; + zpane = (modelDataProvider->getMaxz()-modelDataProvider->getMinz()) / 100 * z; + updateData(); + updateGL(); +} + +void +BasicModelPlot::resetViewPoint() +{ + setRotation(45, 0, 30); // seems most pleasing + setShift(0,0,0); // centre so movement feels natural + setViewportShift(0,0); + setZoom(0.8); // zoom in close but leave space for the axis labels +} + + +/*---------------------------------------------------------------------- + * MODEL PLOT + * Nothing special - just a framed BasicModelPlot + *----------------------------------------------------------------------*/ +ModelPlot::ModelPlot(MainWindow *parent, ModelSettings *settings) : QFrame(parent), main(parent) +{ + // the distinction between a model plot and a basic model plot + // is only to provide a frame for the qwt3d plot (it looks odd + // when compared to the other plots without one) + layout = new QVBoxLayout; + setLineWidth(1); + setFrameStyle(QFrame::Box | QFrame::Raised); + setContentsMargins(0,0,0,0); + basicModelPlot = new BasicModelPlot(parent, settings); + layout->addWidget(basicModelPlot); + layout->setContentsMargins(2,2,2,2); + setLayout(layout); +} + +void +ModelPlot::setStyle(int index) +{ + basicModelPlot->setStyle(index); +} + +void +ModelPlot::setResolution(int val) +{ + basicModelPlot->setResolution(val); +} + +void +ModelPlot::setData(ModelSettings *settings) +{ + basicModelPlot->setData(settings); +} + +void +ModelPlot::resetViewPoint() +{ + basicModelPlot->resetViewPoint(); +} + +void +ModelPlot::setGrid(bool grid) +{ + basicModelPlot->setGrid(grid); +} + +void +ModelPlot::setLegend(bool legend, int coltype) +{ + basicModelPlot->setLegend(legend, coltype); +} + +void +ModelPlot::setFrame(bool frame) +{ + basicModelPlot->setFrame(frame); +} + +void +ModelPlot::setZPane(int z) +{ + basicModelPlot->setZPane(z); +} + + +/*---------------------------------------------------------------------- + * WATER VERTEX ENRICHMENT + * + * THIS IS *NOT* USED IN BAR STYLE. The water enrichment is done by the + * Bar ENRICHMENT INSTEAD. (this is because the alpha values are honored + * amd I cannot work out why). + * + * IF we decide to get rid of surface/grid plots then this enrichment can + * be removed. But maybe someone likes this plot styles. I hate them. + * + *----------------------------------------------------------------------*/ +Water::Water() +{ +} + +void Water::drawBegin() +{ + // diag has been moved to a global variable set by the data model + // this is because the reference variable below is cached + // and is unreliable!! (i.e. bug). + //diag_ = (plot->hull().maxVertex-plot->hull().minVertex).length() * barsize; + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glLineWidth( 0 ); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(0,1); +} + +void Water::drawEnd() +{ + // plot all the nonsense now -- could shade if + // power zones selected on plot (?) + + if (zpane) { + + double minx = plot->hull().minVertex.x; + double miny = plot->hull().minVertex.y; + double maxx = plot->hull().maxVertex.x; + double maxy = plot->hull().maxVertex.y; + double minz = plot->hull().minVertex.z; + double z = zpane + minz; + + // ZPANE SHADING + glColor4f(0.7,0,0,0.4); + glBegin(GL_QUADS); + + // top + glColor4d(0.5,0.5,1,1.0); + glVertex3d(minx,miny,z); + glVertex3d(minx,maxy,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,miny,z); + + // bottom + glColor4d(0.5,0.5,1,1.0); + glVertex3d(minx,miny,minz); + glVertex3d(minx,maxy,minz); + glVertex3d(maxx,maxy,minz); + glVertex3d(maxx,miny,minz); + + // front + glColor4d(0.5,0.5,1,1.0); + glVertex3d(minx,miny,minz); + glVertex3d(minx,miny,z); + glVertex3d(maxx,miny,z); + glVertex3d(maxx,miny,minz); + + // back + glColor4d(0.5,0.5,1,1.0); + glVertex3d(minx,maxy,minz); + glVertex3d(minx,maxy,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,maxy,minz); + + // left + glColor4d(0.5,0.5,1,1.0); + glVertex3d(minx,miny,minz); + glVertex3d(minx,miny,z); + glVertex3d(minx,maxy,z); + glVertex3d(minx,maxy,minz); + + // right + glColor4d(0.5,0.5,1,1.0); + glVertex3d(maxx,miny,minz); + glVertex3d(maxx,miny,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,maxy,minz); + glEnd(); + + glColor3d(0,0,0); + glBegin(GL_LINES); + glVertex3d(minx,miny,z); glVertex3d(minx,maxy, z); + glVertex3d(minx,maxy,z); glVertex3d(maxx,maxy, z); + glVertex3d(maxx,maxy,z); glVertex3d(maxx,miny, z); + glVertex3d(maxx,miny,z); glVertex3d(minx,miny, z); + glEnd(); + } +} + +// draw the enrichment, called for each xyz triple +void Water::draw(Qwt3D::Triple const& ) +{} + +/*---------------------------------------------------------------------- + * BAR VERTEX ENRICHMENT (From here to bottom of source file) + * + * A vertex enrichment to show bars not a surface + * Code courtesy of the enrichment example in the qwtplot3d library and + * hacked to fixup reloading plots etc (the original was a demo example) + * + * CHANGED TO DRAW TWO BARS - wireframe for MAX and shaded for interval + * or wireframe and shaded MAX depending upon + * whether any intervals have been selected. + * + *----------------------------------------------------------------------*/ +Bar::Bar() +{ +} + +void Bar::drawBegin() +{ + // diag has been moved to a global variable set by the data model + // this is because the reference variable below is cached + // and is unreliable!! (i.e. bug). + //diag_ = (plot->hull().maxVertex-plot->hull().minVertex).length() * barsize; + glLineWidth( 0 ); + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(1,1); + +} + +// enrichment pure virtual - we do nothing +void Bar::drawEnd() +{ + // plot all the nonsense now -- could shade if + // power zones selected on plot (?) + if (zpane) { + + double minx = plot->hull().minVertex.x; + double miny = plot->hull().minVertex.y; + double maxx = plot->hull().maxVertex.x; + double maxy = plot->hull().maxVertex.y; + double minz = plot->hull().minVertex.z; + double z = zpane + minz; + + // ZPANE SHADING + glColor3d(0.7,0,0); + glBegin(GL_QUADS); + + // top + glColor4d(0.5,0.5,1,0.7); + glVertex3d(minx,miny,z); + glVertex3d(minx,maxy,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,miny,z); + + // bottom + glColor4d(0.5,0.5,1,0.7); + glVertex3d(minx,miny,minz); + glVertex3d(minx,maxy,minz); + glVertex3d(maxx,maxy,minz); + glVertex3d(maxx,miny,minz); + + // front + glColor4d(0.5,0.5,1,0.7); + glVertex3d(minx,miny,minz); + glVertex3d(minx,miny,z); + glVertex3d(maxx,miny,z); + glVertex3d(maxx,miny,minz); + + // back + glColor4d(0.5,0.5,1,0.7); + glVertex3d(minx,maxy,minz); + glVertex3d(minx,maxy,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,maxy,minz); + + // left + glColor4d(0.5,0.5,1,0.7); + glVertex3d(minx,miny,minz); + glVertex3d(minx,miny,z); + glVertex3d(minx,maxy,z); + glVertex3d(minx,maxy,minz); + + // left + glColor4d(0.5,0.5,1,0.7); + glVertex3d(maxx,miny,minz); + glVertex3d(maxx,miny,z); + glVertex3d(maxx,maxy,z); + glVertex3d(maxx,maxy,minz); + glEnd(); + + glColor3d(0,0,0); + glBegin(GL_LINES); + glVertex3d(minx,miny,z); glVertex3d(minx,maxy, z); + glVertex3d(minx,maxy,z); glVertex3d(maxx,maxy, z); + glVertex3d(maxx,maxy,z); glVertex3d(maxx,miny, z); + glVertex3d(maxx,miny,z); glVertex3d(minx,miny, z); + glEnd(); + } +} + +// draw the enrichment, called for each xyz triple +void Bar::draw(Qwt3D::Triple const& pos) +{ + + // GLStateBewarer sb(GL_LINE_SMOOTH, true); + // sb.turnOn(); + + double interval = plot->hull().maxVertex.z-plot->hull().minVertex.z; + double numlevel = plot->hull().minVertex.z + 1 * interval; + interval /=100; + + GLdouble gminz = plot->hull().minVertex.z; + + // get the colour for this bar from the plot colour provider (defined above) + RGBA rgbat, rgbab; + + // don't plot a thing if it don't mean a thing + if (pos.z == gminz) return; + if (intervals_ == 0) { + // just plot using normal colours (one set of bars per x/y) + rgbat = (*plot->dataColor())(pos); + rgbab = (*plot->dataColor())(pos.x, pos.y, gminz); + } else { + // first bars use max and are see-through + rgbat = RGBA(255,255,255,1); + rgbab = RGBA(255,255,255); + } + + if (intervals_ == 0) { + // shade the max bars if not doing intervals + glBegin(GL_QUADS); + glColor4d(rgbab.r,rgbab.g,rgbab.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + + if (pos.z > numlevel - interval && pos.z < numlevel + interval ) + glColor3d(0.7,0,0); + else + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); + glEnd(); + } + + if (intervals_ == 0 || intervals_&SHOW_FRAME) { + glColor3d(0,0,0); + glBegin(GL_LINES); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); + + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x-diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y-diag_,pos.z); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,pos.z); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); glVertex3d(pos.x-diag_,pos.y+diag_,pos.z); + glEnd(); + } + + // if we have don't have intervals we're done + if (intervals_ == 0) return; + + // get the intervals drawn + // just plot using normal colours (one set of bars per x/y) + rgbat = (*plot->dataColor())(pos); + rgbab = (*plot->dataColor())(pos.x, pos.y, gminz); + + // get pos for the interval data + // call the current data provider + // which is a global + double z = iz.value(xystring(pos.x,pos.y)); + if (z == 0) return; + + // do the max bars + glBegin(GL_QUADS); + glColor4d(rgbab.r,rgbab.g,rgbab.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + + if (z > numlevel - interval && z < numlevel + interval ) + glColor3d(0.7,0,0); + else + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x-diag_,pos.y-diag_,z); + glVertex3d(pos.x+diag_,pos.y-diag_,z); + glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y+diag_,z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y-diag_,z); + glVertex3d(pos.x-diag_,pos.y-diag_,z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y+diag_,z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x-diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y-diag_,z); + + glColor4d(rgbab.r,rgbab.g,rgbat.b,rgbab.a); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glColor4d(rgbat.r,rgbat.g,rgbat.b,rgbat.a); + glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x+diag_,pos.y-diag_,z); + glEnd(); + + glColor3d(0,0,0); + glBegin(GL_LINES); + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y-diag_,gminz); + glVertex3d(pos.x-diag_,pos.y-diag_,z); glVertex3d(pos.x+diag_,pos.y-diag_,z); + glVertex3d(pos.x-diag_,pos.y+diag_,z); glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x-diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,gminz); + glVertex3d(pos.x+diag_,pos.y-diag_,z); glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y-diag_,z); glVertex3d(pos.x-diag_,pos.y+diag_,z); + + glVertex3d(pos.x-diag_,pos.y-diag_,gminz); glVertex3d(pos.x-diag_,pos.y-diag_,z); + glVertex3d(pos.x+diag_,pos.y-diag_,gminz); glVertex3d(pos.x+diag_,pos.y-diag_,z); + glVertex3d(pos.x+diag_,pos.y+diag_,gminz); glVertex3d(pos.x+diag_,pos.y+diag_,z); + glVertex3d(pos.x-diag_,pos.y+diag_,gminz); glVertex3d(pos.x-diag_,pos.y+diag_,z); + glEnd(); +} diff --git a/src/ModelPlot.h b/src/ModelPlot.h new file mode 100644 index 000000000..ad836088f --- /dev/null +++ b/src/ModelPlot.h @@ -0,0 +1,148 @@ +/* + * 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 + */ + +#ifndef _GC_ModelPlot_h +#define _GC_ModelPlot_h 1 + +#include +#include +#include "MainWindow.h" + +#include +#include +#include +#include +#include + +#define MODEL_NONE 0 +#define MODEL_POWER 1 +#define MODEL_CADENCE 2 +#define MODEL_HEARTRATE 3 +#define MODEL_SPEED 4 +#define MODEL_ALT 5 +#define MODEL_PEDALFORCE 6 +#define MODEL_TIME 7 +#define MODEL_DISTANCE 8 +#define MODEL_INTERVAL 9 +#define MODEL_LAT 10 +#define MODEL_LONG 11 +#define MODEL_XYTIME 12 +#define MODEL_POWERZONE 13 + +using namespace Qwt3D; + +// the data provider for the plot +class ModelDataProvider; +class ModelDataColor; +class ModelSettings; +class Bar; +class Water; + +#define STYLE_BAR 1 +#define STYLE_GRID 2 +#define STYLE_SURFACE 3 +#define STYLE_DOTS 4 + +// the core surface plot +class BasicModelPlot : public SurfacePlot +{ + public: + BasicModelPlot(MainWindow *, ModelSettings *); + void setData(ModelSettings *); + void resetViewPoint(); + void setStyle(int); + void setGrid(bool); + void setLegend(bool, int); + void setFrame(bool); + void setZPane(int); + + ModelDataProvider *modelDataProvider; // used by enrichment + + public slots: + + protected: + + // passed from MainWindow + MainWindow *main; + ModelDataColor *modelDataColor; + + Bar *bar; + Water *water; + Qwt3D::PLOTSTYLE surface; + + int currentStyle; +}; + + +// an enrichment for the surface plot to show bars instead of a surface +class Bar : public Qwt3D::VertexEnrichment +{ +public: + Bar(); + + Qwt3D::Enrichment* clone() const {return new Bar(*this);} + + void drawBegin(); + void drawEnd(); + void draw(Qwt3D::Triple const&); + +private: + double level_; + //double diag_; +}; + +// an enrichment for all plot types to "drown" the plot in water +class Water : public Qwt3D::VertexEnrichment +{ + public: + Water(); + Qwt3D::Enrichment* clone() const {return new Water(*this);} + + void drawBegin(); + void drawEnd(); + void draw(Qwt3D::Triple const&); +}; + +// just a frame containing the raw 3d plot (for now) +class ModelPlot : public QFrame +{ + Q_OBJECT + + public: + + + ModelPlot(MainWindow *, ModelSettings *); + void setData(ModelSettings *settings); + void resetViewPoint(); + void setStyle(int); + void setGrid(bool); + void setLegend(bool, int); + void setFrame(bool); + void setZPane(int); + + public slots: + void setResolution(int); + + private: + MainWindow *main; + QVBoxLayout *layout; + BasicModelPlot *basicModelPlot; +}; + + +#endif // _GC_ModelPlot_h diff --git a/src/ModelWindow.cpp b/src/ModelWindow.cpp new file mode 100644 index 000000000..7926515d1 --- /dev/null +++ b/src/ModelWindow.cpp @@ -0,0 +1,331 @@ +/* + * 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 "ModelWindow.h" +#include "ModelPlot.h" +#include "MainWindow.h" +#include "RideItem.h" +#include "IntervalItem.h" +#include "math.h" +#include "Units.h" // for MILES_PER_KM + +#include +#include + +void +ModelWindow::addStandardChannels(QComboBox *box) +{ + box->addItem(tr("None"), MODEL_NONE); + box->addItem(tr("Power"), MODEL_POWER); + box->addItem(tr("Cadence"), MODEL_CADENCE); + box->addItem(tr("Heartrate"), MODEL_HEARTRATE); + box->addItem(tr("Speed"), MODEL_SPEED); + box->addItem(tr("Altitude"), MODEL_ALT); + box->addItem(tr("Pedal Force"), MODEL_PEDALFORCE); + box->addItem(tr("Time"), MODEL_TIME); + box->addItem(tr("Distance"), MODEL_DISTANCE); + //box->addItem(tr("Interval"), MODEL_INTERVAL); //XXX supported differently for now + //box->addItem(tr("Latitude"), MODEL_LAT); //XXX weird values make the plot ugly + //box->addItem(tr("Longitude"), MODEL_LONG); //XXX weird values make the plot ugly +} + +ModelWindow::ModelWindow(MainWindow *parent, const QDir &home) : QWidget(parent), home(home), main(parent) +{ + // Layouts + QVBoxLayout *mainLayout = new QVBoxLayout; + QHBoxLayout *topLayout = new QHBoxLayout; + QHBoxLayout *chartLayout = new QHBoxLayout; + QHBoxLayout *control1Layout = new QHBoxLayout; + QHBoxLayout *control2Layout = new QHBoxLayout; + + // presetValues + presetValues = new QComboBox; + fillPresets(presetValues); + presetValues->setCurrentIndex(1); + + // labels + presetLabel = new QLabel(tr("Analyse"), this); + xLabel = new QLabel(tr("X-Axis:"), this); + yLabel = new QLabel(tr("Y-Axis:"), this); + zLabel = new QLabel(tr("Z-Axis:"), this); + colorLabel = new QLabel(tr("Color:"), this); + binLabel = new QLabel(tr("Bin Width:"), this); + + // selectors + xSelector = new QComboBox; + addStandardChannels(xSelector); + xSelector->setCurrentIndex(1); // power + + ySelector = new QComboBox; + addStandardChannels(ySelector); + ySelector->setCurrentIndex(2); // cadence + + zSelector = new QComboBox; + addStandardChannels(zSelector); + zSelector->addItem(tr("Time at X&Y"), MODEL_XYTIME); + zSelector->setCurrentIndex(9); // time at xy + + colorSelector = new QComboBox; + addStandardChannels(colorSelector); + colorSelector->addItem(tr("Power Zone"), MODEL_POWERZONE); + colorSelector->addItem(tr("Time at X&Y"), MODEL_XYTIME); + colorSelector->setCurrentIndex(9); // time at xy + + styleSelector = new QComboBox; + styleSelector->addItem(tr("Bar")); + styleSelector->addItem(tr("Grid")); + styleSelector->addItem(tr("Surface")); + styleSelector->setCurrentIndex(0); + + ignore = new QCheckBox(tr("Ignore Zero")); + ignore->setChecked(true); + grid = new QCheckBox(tr("Show Grid")); + grid->setChecked(true); + frame = new QCheckBox(tr("Frame Intervals")); + frame->setChecked(true); + legend = new QCheckBox(tr("Legend")); + legend->setChecked(true); + + binWidthLineEdit = new QLineEdit(this); + binWidthLineEdit->setFixedWidth(30); + binWidthLineEdit->setText("5"); + binWidthSlider = new QSlider(Qt::Horizontal); + binWidthSlider->setTickPosition(QSlider::TicksBelow); + binWidthSlider->setTickInterval(1); + binWidthSlider->setMinimum(3); + binWidthSlider->setMaximum(100); + binWidthSlider->setValue(5); + + resetView = new QPushButton(tr("Reset View")); + + // the plot widget + modelPlot= new ModelPlot(main, NULL); + zpane = new QSlider(Qt::Vertical); + zpane->setTickInterval(1); + zpane->setMinimum(0); + zpane->setMaximum(100); + zpane->setValue(0); + + chartLayout->addWidget(zpane); + chartLayout->addWidget(modelPlot); + + // Build Layouts + topLayout->addWidget(presetLabel); + topLayout->addWidget(presetValues); + topLayout->insertStretch(-1); + topLayout->addWidget(grid); + topLayout->addWidget(legend); + topLayout->addWidget(frame); + topLayout->addWidget(styleSelector); + topLayout->setSpacing(10); + + control1Layout->addWidget(xLabel); + control1Layout->addWidget(xSelector); + control1Layout->addWidget(yLabel); + control1Layout->addWidget(ySelector); + control1Layout->addWidget(zLabel); + control1Layout->addWidget(zSelector); + control1Layout->addWidget(colorLabel); + control1Layout->addWidget(colorSelector); + control1Layout->insertStretch(0); + control1Layout->insertStretch(-1); + control1Layout->setSpacing(10); + + control2Layout->addWidget(binLabel); + control2Layout->addWidget(binWidthLineEdit); + control2Layout->addWidget(binWidthSlider); + control2Layout->addWidget(ignore); + control2Layout->addWidget(resetView); + control2Layout->setSpacing(10); + + // Now layout the screen with the new widgets + mainLayout->addItem(topLayout); + mainLayout->addItem(chartLayout); + mainLayout->addItem(control1Layout); + mainLayout->addItem(control2Layout); + mainLayout->setContentsMargins(20, 20, 20, 20); + setLayout(mainLayout); + + // now connect up the widgets + connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected())); + connect(main, SIGNAL(intervalSelected()), this, SLOT(rideSelected())); + connect(presetValues, SIGNAL(currentIndexChanged(int)), this, SLOT(applyPreset(int))); + connect(xSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); + connect(ySelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); + connect(zSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); + connect(colorSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); + connect(grid, SIGNAL(stateChanged(int)), this, SLOT(setGrid())); + connect(legend, SIGNAL(stateChanged(int)), this, SLOT(setLegend())); + connect(frame, SIGNAL(stateChanged(int)), this, SLOT(setFrame())); + connect(styleSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(styleSelected(int))); + connect(ignore, SIGNAL(stateChanged(int)), this, SLOT(setDirty())); + connect(binWidthSlider, SIGNAL(valueChanged(int)), this, SLOT(setBinWidthFromSlider())); + connect(binWidthLineEdit, SIGNAL(editingFinished()), this, SLOT(setBinWidthFromLineEdit())); + connect(resetView, SIGNAL(clicked()), this, SLOT(resetViewPoint())); + connect(zpane, SIGNAL(valueChanged(int)), this, SLOT(setZPane(int))); +} + +void +ModelWindow::rideSelected() +{ + ride = main->rideItem(); + setData(true); +} + +void +ModelWindow::styleSelected(int index) +{ + modelPlot->setStyle(index); // 0 = bar, 1 = surface +} + +void +ModelWindow::setGrid() +{ + modelPlot->setGrid(grid->isChecked()); +} + +void +ModelWindow::setLegend() +{ + modelPlot->setLegend(legend->isChecked(), settings.color); +} + +void +ModelWindow::setFrame() +{ + modelPlot->setFrame(frame->isChecked()); +} +void +ModelWindow::setZPane(int z) +{ + modelPlot->setZPane(z); +} + +void +ModelWindow::intervalSelected() +{ + setData(false); +} + +void +ModelWindow::setData(bool adjustPlot) +{ + settings.ride = ride; + settings.x = xSelector->itemData(xSelector->currentIndex()).toInt(); + settings.y = ySelector->itemData(ySelector->currentIndex()).toInt(); + settings.z = zSelector->itemData(zSelector->currentIndex()).toInt(); + settings.color = colorSelector->itemData(colorSelector->currentIndex()).toInt(); + settings.xbin = binWidthSlider->value(); // XXX fixed to single bin width + settings.ybin = binWidthSlider->value(); // XXX due to issues with bar geometry + settings.crop = false; // XXX not implemented + settings.zpane = 0; + settings.ignore = ignore->isChecked(); + settings.gridlines = grid->isChecked(); + settings.frame = frame->isChecked(); + settings.legend = legend->isChecked(); + settings.adjustPlot = adjustPlot; + zpane->setValue(0); // reset it! + + // any intervals to plot? + settings.intervals.clear(); + for (int i=0; iallIntervalItems()->childCount(); i++) { + IntervalItem *current = dynamic_cast(main->allIntervalItems()->child(i)); + if (current != NULL && current->isSelected() == true) + settings.intervals.append(current); + } + modelPlot->setData(&settings); + setClean(); +} + +void +ModelWindow::setBinWidthFromSlider() +{ + binWidthLineEdit->setText(QString("%1").arg(binWidthSlider->value())); + setDirty(); +} + +void +ModelWindow::setBinWidthFromLineEdit() +{ + binWidthSlider->setValue(binWidthLineEdit->text().toInt()); + setDirty(); +} + +void +ModelWindow::resetViewPoint() +{ + // either replot or center after fiddling + if (dirty) setData(true); + else modelPlot->resetViewPoint(); +} + +void +ModelWindow::setDirty() +{ + dirty = true; + resetView->setText(tr("Plot")); +} + +void +ModelWindow::setClean() +{ + dirty = false; + resetView->setText(tr("Reset View")); +} + +// +// Prepare some preset analysis +// +static struct preset { + QString name; // QComboBox value + int x, y, z, color; // values for xselector, yselector and zselector and color + bool ignore; + int bin; // value for binwidth +} presets[] = { + + { "User Defined", 0, 0, 0, 0, true, 20 }, + { "Natural Cadence Selection", 1, 2, 9, 9, false, 5 }, // don't ignore zero for cadences! + { "Power Fatigue", 8, 1, 9, 9, true, 5 }, + { "Impact of Altitude", 5, 3, 1, 9, true, 10 }, + { "", 0, 0, 0, 0, false, 0 } +}; + +void +ModelWindow::applyPreset(int index) +{ + if (index >=0) { + xSelector->setCurrentIndex(presets[index].x); + ySelector->setCurrentIndex(presets[index].y); + zSelector->setCurrentIndex(presets[index].z); + colorSelector->setCurrentIndex(presets[index].color); + ignore->setChecked(presets[index].ignore); + binWidthSlider->setValue(presets[index].bin); + binWidthLineEdit->setText(QString("%1").arg(presets[index].bin)); + + setDirty(); + if (index) setData(true); + } +} + +void +ModelWindow::fillPresets(QComboBox *p) +{ + for (int i=0; presets[i].name != ""; i++) { + p->addItem(presets[i].name); + } +} diff --git a/src/ModelWindow.h b/src/ModelWindow.h new file mode 100644 index 000000000..4a2a205da --- /dev/null +++ b/src/ModelWindow.h @@ -0,0 +1,123 @@ +/* + * 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 + */ + +#ifndef _GC_ModelWindow_h +#define _GC_ModelWindow_h 1 + +#include +#include +#include "MainWindow.h" + +class ModelPlot; // we don't include the header because it uses namespaces +class ModelDataColor; + +class ModelSettings +{ + public: + RideItem *ride; // ride to use + int x,y,z,color; // which channels to use + int xbin, ybin; // bin size + bool crop, // crop to non-zero z values + ignore, // ignore zeroes on x or y + adjustPlot, // reset plot settings to dataset + gridlines, // show gridlines + frame, // max z when showing intervals? + legend; // display a legend? + int zpane; // where to show zpane 0-100% of the z-axis + QList intervals; // intervals to apply + + ModelDataColor *colorProvider; // color model to update +}; + + +class ModelWindow : public QWidget +{ + Q_OBJECT + + public: + + ModelWindow(MainWindow *, const QDir &); + + public slots: + void rideSelected(); + void intervalSelected(); + void applyPreset(int); + void setData(bool); + void setGrid(); + void setLegend(); + void setFrame(); + void setZPane(int); + void resetViewPoint(); + void setBinWidthFromSlider(); + void setBinWidthFromLineEdit(); + void styleSelected(int); + void setDirty(); + void setClean(); + + protected: + + // passed from MainWindow + QDir home; + MainWindow *main; + bool useMetricUnits; + + bool dirty; // settings changed but not reploted + ModelSettings settings; // last used settings + + // Ride to plot - captured from rideSelected signal + RideItem *ride; + + // layout + ModelPlot *modelPlot; + + // labels + QLabel *presetLabel, + *xLabel, + *yLabel, + *zLabel, + *colorLabel, + *binLabel; + + // top of screen selectors + QComboBox *presetValues; + + // bottom selectors + QComboBox *xSelector, + *ySelector, + *zSelector, + *colorSelector, + *styleSelector; + QCheckBox *ignore, + *grid, + *frame, + *legend; + QPushButton *resetView; + + QLineEdit *binWidthLineEdit; + QSlider *binWidthSlider; + + // z pane slider + QSlider *zpane; + + private: + + void addStandardChannels(QComboBox *); + void fillPresets(QComboBox *); +}; + +#endif // _GC_ModelWindow_h diff --git a/src/gcconfig.pri.in b/src/gcconfig.pri.in index e3f527042..83e4d6792 100644 --- a/src/gcconfig.pri.in +++ b/src/gcconfig.pri.in @@ -6,6 +6,14 @@ BOOST_INSTALL = /usr/local/boost SRMIO_INSTALL = /usr/local/srmio D2XX_INCLUDE = /usr/local/include/D2XX +# If you want 3D plotting, you need to install qwtplot3d +# +# http://qwtplot3d.sourceforge.net/ +# +# then set the following variable appropriately: + +#QWT3D_INSTALL = /usr/local/qwtplot3d + # We recommend a debug build for development, and a static build for releases. CONFIG += debug #CONFIG += static diff --git a/src/src.pro b/src/src.pro index 5da8665fd..12f8e5f95 100644 --- a/src/src.pro +++ b/src/src.pro @@ -24,6 +24,15 @@ LIBS += -lm SOURCES += SrmDevice.cpp } +!isEmpty( QWT3D_INSTALL) { + INCLUDEPATH += $${QWT3D_INSTALL}/include + LIBS += $${QWT3D_INSTALL}/lib/libqwtplot3d.a + QT += opengl + HEADERS += ModelPlot.h ModelWindow.h + SOURCES += ModelPlot.cpp ModelWindow.cpp + DEFINES += GC_HAVE_QWTPLOT3D +} + macx { LIBS += -framework Carbon }