Пример #1
0
// delete a range, extend an adjacent (prior if available, otherwise next)
// range to cover the same time period, then return the number of the new range
// covering the date range of the deleted range
int Zones::deleteRange(int rnum) {
    assert((ranges.size() > 1) && (rnum >= 0) && (rnum < ranges.size()));
    int return_rnum;

    // extend an adjacent range
    if (rnum == 0) {
	return_rnum = 0;
	setStartDate(rnum + 1, getStartDate(rnum));
    }
    else {
	return_rnum = rnum - 1;
	setEndDate(return_rnum, getEndDate(rnum));
    }

    // eliminate the allocation in the present range
    delete ranges[rnum];

    // drop higher ranges down one slot
    for (int r = rnum; r < ranges.size() - 1; r ++)
	ranges[r] = ranges[r + 1];

    // reduce the number of ranges by one
    ranges.removeLast();

    return return_rnum;
}
Пример #2
0
// insert a new range starting at the given date extending to the end of the zone currently
// containing that date.  If the start date of that zone is prior to the specified start
// date, then that zone range is shorted.
int HrZones::insertRangeAtDate(QDate date, int lt) {
    assert(date.isValid());
    int rnum;

    if (ranges.empty()) {
        addHrZoneRange();
        rnum = 0;
    }
    else {
        rnum = whichRange(date);
        assert(rnum >= 0);
        QDate date1 = getStartDate(rnum);

        // if the old range has dates before the specified, then truncate
        // the old range and shift up the existing ranges
        if (date > date1) {
            QDate endDate = getEndDate(rnum);
            setEndDate(rnum, date);
            ranges.insert(++ rnum, HrZoneRange(date, endDate));
        }
    }

    if (lt > 0) {
        setLT(rnum, lt);
        setHrZonesFromLT(rnum);
    }

    return rnum;
}
Пример #3
0
/*
 * Copyright (c) 2010 Damien Grauser ([email protected])
 *
 * 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 <QMessageBox>
#include "HrZones.h"
#include "Colors.h"
#include "TimeUtils.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <cmath>


// the infinity endpoints are indicated with extreme date ranges
// but not zero dates so we can edit and compare them
static const QDate date_zero(1900, 01, 01);
static const QDate date_infinity(9999,12,31);

// initialize default static zone parameters
void HrZones::initializeZoneParameters()
{
    static int initial_zone_default[] = {
        0, 68, 83, 94, 105
    };
    static double initial_zone_default_trimp[] = {
        0.9, 1.1, 1.2, 2.0, 5.0
    };
    static const QString initial_zone_default_desc[] = {
        tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"),
        tr("VO2Max")
    };
    static const char *initial_zone_default_name[] = {
        "Z1", "Z2", "Z3", "Z4", "Z5"
    };

    static int initial_nzones_default =
    sizeof(initial_zone_default) /
    sizeof(initial_zone_default[0]);

    if (run) {
        fileName_ = "run-hr.zones";
    } else {
        fileName_ = "hr.zones";
    }

    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_trimp.clear();
    scheme.nzones_default = 0;

    scheme.nzones_default = initial_nzones_default;

    for (int z = 0; z < scheme.nzones_default; z ++) {
        scheme.zone_default.append(initial_zone_default[z]);
        scheme.zone_default_is_pct.append(true);
        scheme.zone_default_name.append(QString(initial_zone_default_name[z]));
        scheme.zone_default_desc.append(QString(initial_zone_default_desc[z]));
        scheme.zone_default_trimp.append(initial_zone_default_trimp[z]);
    }
}

// read zone file, allowing for zones with or without end dates
bool HrZones::read(QFile &file)
{

    //
    // GET SET
    //
    defaults_from_user = false;
    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_trimp.clear();
    scheme.nzones_default = 0;
    ranges.clear();

    // set up possible warning dialog
    warning = QString();
    int warning_lines = 0;
    const int max_warning_lines = 100;

    // macro to append lines to the warning
    #define append_to_warning(s) \
        if (warning_lines < max_warning_lines) \
        warning.append(s);  \
        else if (warning_lines == max_warning_lines) \
        warning.append("...\n"); \
        warning_lines ++;

    // read using text mode takes care of end-lines
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        err = tr("can't open %1 file").arg(fileName_);
        return false;
    }
    QTextStream fileStream(&file);

    QRegExp commentrx("\\s*#.*$");
    QRegExp blankrx("^[ \t]*$");
    QRegExp rangerx[] = {
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?"                // optional {LT = integer (optional %)}
                "\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?"            // optional {RestHr = integer (optional %)}
                "\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?"             // optional {MaxHr = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive),
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s+(?:until|to|-)\\s+"                             // until
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?"  // end date
                "\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?"                 // optional {LT = integer (optional %)}
                "\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?"             // optional {RestHr = integer (optional %)}
                "\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?"              // optional {MaxHr = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive)
    };
    QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
           "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$",
           Qt::CaseInsensitive);//
    QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
              Qt::CaseInsensitive);

    int lineno = 0;

    // the current range in the file
    // ZoneRange *range = NULL;
    bool in_range = false;
    QDate begin = date_zero, end = date_infinity;
    int lt = 0;
    int restHr = 0;
    int maxHr = 0;
    QList<HrZoneInfo> zoneInfos;

    // true if zone defaults are found in the file (then we need to write them)
    bool zones_are_defaults = false;

    //
    // READ IN hr.zones FILE
    //

    // loop through line by line
    while (!fileStream.atEnd()) {

        // starting from line 1
        ++lineno;

        // get a line in
        QString line = fileStream.readLine();
        int pos = commentrx.indexIn(line, 0);

        // strip comments
        if (pos != -1) line = line.left(pos);

        // its a blank line (we check after comments stripped)
        if (blankrx.indexIn(line, 0) == 0) goto next_line; // who wrote this? bleck.

        // check for default zone range definition (may be followed by hr zone definitions)
        if (zonedefaultsx.indexIn(line, 0) != -1) {
            zones_are_defaults = true;

            // defaults are allowed only at the beginning of the file
            if (ranges.size()) {
                err = tr("HR Zone defaults must be specified at head of %1 file").arg(fileName_);
                return false;
            }

            // only one set of defaults is allowed
            if (scheme.nzones_default) {
                err = tr("Only one set of zone defaults may be specified in %1 file").arg(fileName_);
                return false;
            }

            // ok move on to get defaults setup
            goto next_line;
        }

        // check for range specification (may be followed by zone definitions)
        for (int r=0; r<2; r++) {

            if (rangerx[r].indexIn(line, 0) != -1) {

                if (in_range) {

                    // if zones are empty, then generate them
                    HrZoneRange range(begin, end, lt, restHr, maxHr);
                    range.zones = zoneInfos;

                    if (range.zones.empty()) {
                        if (range.lt > 0) setHrZonesFromLT(range);
                        else {
                            err = tr("line %1: read new range without reading "
                                    "any zones for previous one").arg(lineno);
                            file.close();
                            return false;
                        }
                    } else {
                        qSort(range.zones);
                    }
                    ranges.append(range);
                }

                in_range = true;
                zones_are_defaults = false;
                zoneInfos.clear();

                // process the beginning date
                if (rangerx[r].cap(1) == "BEGIN")
                    begin = date_zero;
                else {
                    begin = QDate(rangerx[r].cap(2).toInt(),
                    rangerx[r].cap(3).toInt(),
                    rangerx[r].cap(4).toInt());
                }

                // process an end date, if any, else it is null
                if (rangerx[r].cap(5) == "END") end = date_infinity;
                else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) {

                    end = QDate(rangerx[r].cap(6).toInt(),
                    rangerx[r].cap(7).toInt(),
                    rangerx[r].cap(8).toInt());

                } else {
                    end = QDate();
                }

                // set up the range, capturing LT if it's specified
                // range = new ZoneRange(begin, end);
                int nLT = (r ? 11 : 7);
                if (rangerx[r].captureCount() >= (nLT)) lt = rangerx[r].cap(nLT).toInt();
                else lt = 0;

                int nRestHr = (r ? 14 : 10);
                if (rangerx[r].captureCount() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt();
                else restHr = 0;

                int nMaxHr = (r ? 17 : 13);
                if (rangerx[r].captureCount() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt();
                else maxHr = 0;

                // bleck
                goto next_line;
            }
        }

        // check for zone definition
        if (zonerx.indexIn(line, 0) != -1) {

            if (! (in_range || zones_are_defaults)) {
                err = tr("line %1: read zone without preceding date range").arg(lineno);
                file.close();
                return false;
            }

            int lo = zonerx.cap(3).toInt();
            double trimp = zonerx.cap(5).toDouble();

            // allow for zone specified as % of LT
            bool lo_is_pct = false;
            if (zonerx.cap(4) == "%") {
                if (zones_are_defaults) lo_is_pct = true;
                else if (lt > 0) lo = int(lo * lt / 100);
                else {
                    err = tr("attempt to set zone based on % of LT without setting LT in line number %1.\n").
                    arg(lineno);
                    file.close();
                    return false;
                }
            }

            int hi = -1; // signal an undefined number
            double tr =  zonerx.cap(5).toDouble();

            if (zones_are_defaults) {
                scheme.nzones_default ++;
                scheme.zone_default_is_pct.append(lo_is_pct);
                scheme.zone_default.append(lo);
                scheme.zone_default_name.append(zonerx.cap(1));
                scheme.zone_default_desc.append(zonerx.cap(2));
                scheme.zone_default_trimp.append(trimp);
                defaults_from_user = true;

            } else {

                HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr);
                zoneInfos.append(zone);
            }
        }

        next_line: ;
    }

    // did we drop out mid way through ?
    if (in_range) {
        HrZoneRange range(begin, end, lt, restHr, maxHr);
        range.zones = zoneInfos;
        if (range.zones.empty()) {
            if (range.lt > 0)
                setHrZonesFromLT(range);
            else {
                err = tr("file ended without reading any zones for last range");
                file.close();
                return false;
            }
        } else {

            qSort(range.zones);
        }
        ranges.append(range);
    }

    // reading done
    file.close();

    // sort the ranges
    qSort(ranges);

    //
    // POST-PROCESS / FIX-UP ZONES
    //

    // set the default zones if not in file
    if (!scheme.nzones_default)  {

        // do we have a zone which is explicitly set?
        for (int i=0; i<ranges.count(); i++) {
            if (ranges[i].hrZonesSetFromLT == false) {
                // set the defaults using this one!
                scheme.nzones_default = ranges[i].zones.count();
                for (int j=0; j<scheme.nzones_default; j++) {
                    scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].lt) * 100.00);
                    scheme.zone_default_is_pct.append(true);
                    scheme.zone_default_name.append(ranges[i].zones[j].name);
                    scheme.zone_default_desc.append(ranges[i].zones[j].desc);
                    scheme.zone_default_trimp.append(ranges[i].zones[j].trimp);
                }
            }
        }

        // still not set then reset to defaults as usual
        if (!scheme.nzones_default) initializeZoneParameters();
    }

    // resolve undefined endpoints in ranges and zones
    for (int nr = 0; nr < ranges.size(); nr ++) {

        // clean up gaps or overlaps in zone ranges
        if (ranges[nr].end.isNull()) {

            ranges[nr].end = (nr < ranges.size() - 1) ?  ranges[nr + 1].begin : date_infinity;

         } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) {

            append_to_warning(tr("Setting end date of range %1 to start date of range %2.\n").arg(nr + 1).arg(nr + 2));
            ranges[nr].end = ranges[nr + 1].begin;

        } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) {


            append_to_warning(tr("Extending final range %1 to infinite to include present date.\n").arg(nr + 1));
            ranges[nr].end = date_infinity;
        }

        if (ranges[nr].lt <= 0) {
            err = tr("LT must be greater than zero in zone range %1 of %2").arg(nr + 1).arg(fileName_);
            return false;
        }

        if (ranges[nr].zones.size()) {

            // check that the first zone starts with zero
            // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this, so removing it.

            // resolve zone end powers
            for (int nz = 0; nz < ranges[nr].zones.size(); nz ++) {
                if (ranges[nr].zones[nz].hi == -1) 
                    ranges[nr].zones[nz].hi = (nz < ranges[nr].zones.size() - 1) ?  
                                                ranges[nr].zones[nz + 1].lo : INT_MAX;

                else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) {

                    if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) {

                        append_to_warning(tr("Range %1: matching top of zone %2 (%3) to bottom of zone %4 (%5).\n")
                              .arg(nr+1).arg(ranges[nr].zones[nz].name).arg(ranges[nr].zones[nz].hi)
                              .arg(ranges[nr].zones[nz + 1].name) .arg(ranges[nr].zones[nz + 1].lo));
                    }
                    ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo;

                } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) {

                    append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n")
                                     .arg(nr + 1).arg(ranges[nr].zones[nz].name).arg(ranges[nr].zones[nz].hi));
                    ranges[nr].zones[nz].hi = INT_MAX;
                }
            }
        }
    }

    // mark zones as modified so pages which depend on zones can be updated
    modificationTime = QDateTime::currentDateTime();

    return true;
}

// note empty dates are treated as automatic matches for begin or
// end of range
int HrZones::whichRange(const QDate &date) const
{
    for (int rnum = 0; rnum < ranges.size(); ++rnum) {
        const HrZoneRange &range = ranges[rnum];
        if (((date >= range.begin) || (range.begin.isNull())) &&
            ((date < range.end) || (range.end.isNull())))
            return rnum;
    }
    return -1;
}

int HrZones::numZones(int rnum) const
{
    if (rnum < 0 || rnum >= ranges.size()) return 0;
    return ranges[rnum].zones.size();
}

int HrZones::whichZone(int rnum, double value) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    const HrZoneRange &range = ranges[rnum];
    for (int j = 0; j < range.zones.size(); ++j) {
        const HrZoneInfo &info = range.zones[j];
    // note: the "end" of range is actually in the next zone
        if ((value >= info.lo) && (value < info.hi))
            return j;
    }

    // if we got here either it is negative, nan, inf or way high
    return -1;
}

void HrZones::zoneInfo(int rnum, int znum,
                     QString &name, QString &description,
                     int &low, int &high, double &trimp) const
{
    assert(rnum < ranges.size());
    const HrZoneRange &range = ranges[rnum];
    assert(znum < range.zones.size());
    const HrZoneInfo &zone = range.zones[znum];
    name = zone.name;
    description = zone.desc;
    low = zone.lo;
    high = zone.hi;
    trimp= zone.trimp;
}

int HrZones::getLT(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].lt;
}

void HrZones::setLT(int rnum, int lt)
{
    ranges[rnum].lt = lt;
    modificationTime = QDateTime::currentDateTime();
}

// generate a list of zones from LT
int HrZones::lowsFromLT(QList <int> *lows, int lt) const {

    lows->clear();

    for (int z = 0; z < scheme.nzones_default; z++)
    lows->append(scheme.zone_default_is_pct[z] ?
                scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]);

    return scheme.nzones_default;
}

int HrZones::getRestHr(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].restHr;
}

void HrZones::setRestHr(int rnum, int restHr)
{
    ranges[rnum].restHr = restHr;
    modificationTime = QDateTime::currentDateTime();
}

int HrZones::getMaxHr(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].maxHr;
}

void HrZones::setMaxHr(int rnum, int maxHr)
{
    ranges[rnum].maxHr = maxHr;
    modificationTime = QDateTime::currentDateTime();
}

// access the zone name
QString HrZones::getDefaultZoneName(int z) const {
    return scheme.zone_default_name[z];
}

// access the zone description
QString HrZones::getDefaultZoneDesc(int z) const {
    return scheme.zone_default_desc[z];
}

// set the zones from the LT value (the cp variable)
void HrZones::setHrZonesFromLT(HrZoneRange &range) {
    range.zones.clear();

    if (scheme.nzones_default == 0)
    initializeZoneParameters();

    for (int i = 0; i < scheme.nzones_default; i++) {
    int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i];
    int hi = lo;
    double trimp = scheme.zone_default_trimp[i];

    HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp);
    range.zones.append(zone);
    }

    // sort the zones (some may be pct, others absolute, so zones need to be sorted,
    // rather than the defaults
    qSort(range.zones);

    // set zone end dates
    for (int i = 0; i < range.zones.size(); i ++)
    range.zones[i].hi =
        (i < scheme.nzones_default - 1) ?
        range.zones[i + 1].lo :
        INT_MAX;

    // mark that the zones were set from LT, so if zones are subsequently
    // written, only LT is saved
    range.hrZonesSetFromLT = true;
}

void HrZones::setHrZonesFromLT(int rnum) {
    assert((rnum >= 0) && (rnum < ranges.size()));
    setHrZonesFromLT(ranges[rnum]);
}

// return the list of starting values of zones for a given range
QList <int> HrZones::getZoneLows(int rnum) const {
    if (rnum >= ranges.size())
        return QList <int>();
    const HrZoneRange &range = ranges[rnum];
    QList <int> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].lo);
    return return_values;
}

// return the list of ending values of zones for a given range
QList <int> HrZones::getZoneHighs(int rnum) const {
    if (rnum >= ranges.size())
        return QList <int>();
    const HrZoneRange &range = ranges[rnum];
    QList <int> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].hi);
    return return_values;
}

// return the list of zone names
QList <QString> HrZones::getZoneNames(int rnum) const {
    if (rnum >= ranges.size())
        return QList <QString>();
    const HrZoneRange &range = ranges[rnum];
    QList <QString> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].name);
    return return_values;
}

// return the list of zone trimp coef
QList <double> HrZones::getZoneTrimps(int rnum) const {
    if (rnum >= ranges.size())
        return QList <double>();
    const HrZoneRange &range = ranges[rnum];
    QList <double> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].trimp);
    return return_values;
}

QString HrZones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const
{
    assert(rnum < ranges.size());
    const HrZoneRange &range = ranges[rnum];
    if (time_in_zone.size() < range.zones.size()) return "";
    QString summary;
    if(range.lt > 0){
        summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
        summary += "<tr><td align=\"center\">";
        summary += tr("Threshold (bpm): %1").arg(range.lt);
        summary += "</td></tr></table>";
    }
    summary += "<table align=\"center\" width=\"70%\" ";
    summary += "border=\"0\">";
    summary += "<tr>";
    summary += tr("<td align=\"center\">Zone</td>");
    summary += tr("<td align=\"center\">Description</td>");
    summary += tr("<td align=\"center\">Low (bpm)</td>");
    summary += tr("<td align=\"center\">High (bpm)</td>");
    summary += tr("<td align=\"center\">Time</td>");
    summary += tr("<td align=\"center\">%</td>");
    summary += "</tr>";

    double duration = 0;
    foreach(double v, time_in_zone) { duration += v; }

    for (int zone = 0; zone < time_in_zone.size(); ++zone) {
        if (time_in_zone[zone] > 0.0) {
            QString name, desc;
            int lo, hi;
            double trimp;
            zoneInfo(rnum, zone, name, desc, lo, hi, trimp);
            if (zone % 2 == 0)
                summary += "<tr bgcolor='" + color.name() + "'>";
            else
                summary += "<tr>";
            summary += QString("<td align=\"center\">%1</td>").arg(name);
            summary += QString("<td align=\"center\">%1</td>").arg(desc);
            summary += QString("<td align=\"center\">%1</td>").arg(lo);
            if (hi == INT_MAX)
                summary += "<td align=\"center\">MAX</td>";
            else
                summary += QString("<td align=\"center\">%1</td>").arg(hi);
            summary += QString("<td align=\"center\">%1</td>")
                .arg(time_to_string((unsigned) round(time_in_zone[zone])));
            summary += QString("<td align=\"center\">%1</td>")
                .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0);
            summary += "</tr>";
        }
    }
    summary += "</table>";
    return summary;
}

#define USE_SHORT_POWER_ZONES_FORMAT true   /* whether a less redundent format should be used */
void HrZones::write(QDir home)
{
    QString strzones;

    // always write the defaults (config pane can adjust)
    strzones += QString("DEFAULTS:\n");
    for (int z = 0 ; z < scheme.nzones_default; z ++)
        strzones += QString("%1,%2,%3%4,%5\n").
        arg(scheme.zone_default_name[z]).
        arg(scheme.zone_default_desc[z]).
        arg(scheme.zone_default[z]).
        arg(scheme.zone_default_is_pct[z]?"%":"").
        arg(scheme.zone_default_trimp[z]);
    strzones += QString("\n");

    for (int i = 0; i < ranges.size(); i++) {
        int lt = getLT(i);
        int restHr = getRestHr(i);
        int maxHr = getMaxHr(i);

    // print header for range
    // note this explicitly sets the first and last ranges such that all time is spanned

        // note: BEGIN is not needed anymore
        //       since it becomes Jan 01 1900
        strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr);
    strzones += QString("\n");

    // step through and print the zones if they've been explicitly set
    if (! ranges[i].hrZonesSetFromLT) {
        for (int j = 0; j < ranges[i].zones.size(); j ++) {
                const HrZoneInfo &zi = ranges[i].zones[j];
        strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp);
            }
        strzones += QString("\n");
    }
    }

    QFile file(home.canonicalPath() + "/" + fileName_);
    if (file.open(QFile::WriteOnly))
    {
        QTextStream stream(&file);
        stream << strzones;
        file.close();
    } else {
        QMessageBox msgBox;
        msgBox.setIcon(QMessageBox::Critical);
        msgBox.setText(tr("Problem Saving Heartrate Zones"));
        msgBox.setInformativeText(tr("File: %1 cannot be opened for 'Writing'. Please check file properties.").arg(home.canonicalPath() + "/" + fileName_));
        msgBox.exec();
        return;
    }
}
Пример #4
0
unsigned int WriMoImp::getRunningTotal(const Date& date) const
{
	unsigned int runningTotal = 0;

	for (Date currentDate = getStartDate(); currentDate <= date;
			currentDate = currentDate.next())
	{
		runningTotal += getWordCount(currentDate);
	}

	return runningTotal;
}
void
TestRecurrentTransaction::testGetStartDate() {
    QFETCH(QDate, startDate);

    auto amount = .45;
    auto account = std::make_shared<PublicAccount>("Test account", .0, "");
    auto category = std::make_shared<com::chancho::Category>("Sushi", com::chancho::Category::Type::EXPENSE);
    auto transactionPtr = std::make_shared<com::chancho::Transaction>(account, amount, category);
    auto recurrentPtr = std::make_shared<com::chancho::RecurrentTransaction>(transactionPtr,
        std::make_shared<com::chancho::RecurrentTransaction::Recurrence>(
                com::chancho::RecurrentTransaction::Recurrence::Defaults::DAILY, startDate));

    auto qmlTransaction = std::make_shared<com::chancho::tests::PublicRecurrentTransaction>(recurrentPtr);
    QCOMPARE(qmlTransaction->getStartDate(), startDate);
}
Пример #6
0
/*
 * Copyright (c) 2010 Damien Grauser ([email protected])
 *
 * 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 <QMessageBox>
#include "HrZones.h"
#include "Colors.h"
#include "TimeUtils.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <math.h>
#include <boost/crc.hpp>


// the infinity endpoints are indicated with extreme date ranges
// but not zero dates so we can edit and compare them
static const QDate date_zero(1900, 01, 01);
static const QDate date_infinity(9999,12,31);

// initialize default static zone parameters
void HrZones::initializeZoneParameters()
{
    static int initial_zone_default[] = {
        0, 68, 83, 94, 105
    };
    static double initial_zone_default_trimp[] = {
        0.9, 1.1, 1.2, 2.0, 5.0
    };
    static const QString initial_zone_default_desc[] = {
        tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"),
        tr("VO2Max")
    };
    static const char *initial_zone_default_name[] = {
        "Z1", "Z2", "Z3", "Z4", "Z5"
    };

    static int initial_nzones_default =
    sizeof(initial_zone_default) /
    sizeof(initial_zone_default[0]);

    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_trimp.clear();
    scheme.nzones_default = 0;

    scheme.nzones_default = initial_nzones_default;

    for (int z = 0; z < scheme.nzones_default; z ++) {
        scheme.zone_default.append(initial_zone_default[z]);
        scheme.zone_default_is_pct.append(true);
        scheme.zone_default_name.append(QString(initial_zone_default_name[z]));
        scheme.zone_default_desc.append(QString(initial_zone_default_desc[z]));
        scheme.zone_default_trimp.append(initial_zone_default_trimp[z]);
    }
}

// read zone file, allowing for zones with or without end dates
bool HrZones::read(QFile &file)
{
    defaults_from_user = false;
    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_trimp.clear();
    scheme.nzones_default = 0;
    ranges.clear();

    // set up possible warning dialog
    warning = QString();
    int warning_lines = 0;
    const int max_warning_lines = 100;

    // macro to append lines to the warning
    #define append_to_warning(s) \
        if (warning_lines < max_warning_lines) \
        warning.append(s);  \
        else if (warning_lines == max_warning_lines) \
        warning.append("...\n"); \
        warning_lines ++;

    // read using text mode takes care of end-lines
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        err = "can't open file";
        return false;
    }
    QTextStream fileStream(&file);

    QRegExp commentrx("\\s*#.*$");
    QRegExp blankrx("^[ \t]*$");
    QRegExp rangerx[] = {
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?"                // optional {LT = integer (optional %)}
                "\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?"            // optional {RestHr = integer (optional %)}
                "\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?"             // optional {MaxHr = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive),
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s+(?:until|to|-)\\s+"                             // until
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?"  // end date
                "\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?"                 // optional {LT = integer (optional %)}
                "\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?"             // optional {RestHr = integer (optional %)}
                "\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?"              // optional {MaxHr = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive)
    };
    QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
           "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$",
           Qt::CaseInsensitive);//
    QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
              Qt::CaseInsensitive);

    int lineno = 0;

    // the current range in the file
    // ZoneRange *range = NULL;
    bool in_range = false;
    QDate begin = date_zero, end = date_infinity;
    int lt = 0;
    int restHr = 0;
    int maxHr = 0;
    QList<HrZoneInfo> zoneInfos;

    // true if zone defaults are found in the file (then we need to write them)
    bool zones_are_defaults = false;

    while (! fileStream.atEnd() ) {
        ++lineno;
    QString line = fileStream.readLine();
        int pos = commentrx.indexIn(line, 0);
        if (pos != -1)
            line = line.left(pos);
        if (blankrx.indexIn(line, 0) == 0)
        goto next_line;

    // check for default zone range definition (may be followed by hr zone definitions)
    if (zonedefaultsx.indexIn(line, 0) != -1) {
        zones_are_defaults = true;

        // defaults are allowed only at the beginning of the file
        if (ranges.size()) {
        err = "HR Zone defaults must be specified at head of hr.zones file";
        return false;
        }

        // only one set of defaults is allowed
        if (scheme.nzones_default) {
        err = "Only one set of zone defaults may be specified in hr.zones file";
        return false;
        }

        goto next_line;
    }

    // check for range specification (may be followed by zone definitions)
        for (int r=0; r<2; r++) {
                if (rangerx[r].indexIn(line, 0) != -1) {

                        if (in_range) {

                                // if zones are empty, then generate them
                                HrZoneRange range(begin, end, lt, restHr, maxHr);
                                range.zones = zoneInfos;

                                if (range.zones.empty()) {
                                        if (range.lt > 0) setHrZonesFromLT(range);
                                        else {
                                                err = tr("line %1: read new range without reading "
                                                "any zones for previous one").arg(lineno);
                                                file.close();
                                                return false;
                                        }
                                } else {
                                        qSort(range.zones);
                                }
                                ranges.append(range);
                        }

                        in_range = true;
                        zones_are_defaults = false;
                        zoneInfos.clear();

                        // process the beginning date
                        if (rangerx[r].cap(1) == "BEGIN")
                                begin = date_zero;
                        else {
                                begin = QDate(rangerx[r].cap(2).toInt(),
                                rangerx[r].cap(3).toInt(),
                                rangerx[r].cap(4).toInt());
                        }

                        // process an end date, if any, else it is null
                        if (rangerx[r].cap(5) == "END") end = date_infinity;
                        else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() ||
                                 rangerx[r].cap(8).toInt()) {

                                end = QDate(rangerx[r].cap(6).toInt(),
                                rangerx[r].cap(7).toInt(),
                                rangerx[r].cap(8).toInt());

                        } else {
                                end = QDate();
                        }

                        // set up the range, capturing LT if it's specified
                        // range = new ZoneRange(begin, end);
                        int nLT = (r ? 11 : 7);
                        if (rangerx[r].numCaptures() >= (nLT)) lt = rangerx[r].cap(nLT).toInt();
                        else lt = 0;

                        int nRestHr = (r ? 14 : 10);
                        if (rangerx[r].numCaptures() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt();
                        else restHr = 0;

                        int nMaxHr = (r ? 17 : 13);
                        if (rangerx[r].numCaptures() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt();
                        else maxHr = 0;

                        // bleck
                        goto next_line;
                }
        }

        // check for zone definition
        if (zonerx.indexIn(line, 0) != -1) {
           if (! (in_range || zones_are_defaults)) {
              err = tr("line %1: read zone without "
                "preceeding date range").arg(lineno);
              file.close();
              return false;
           }

           int lo = zonerx.cap(3).toInt();
           double trimp = zonerx.cap(5).toDouble();

           // allow for zone specified as % of LT
           bool lo_is_pct = false;
           if (zonerx.cap(4) == "%") {
           if (zones_are_defaults) lo_is_pct = true;
           else if (lt > 0) lo = int(lo * lt / 100);
           else {
              err = tr("attempt to set zone based on % of "
                       "LT without setting LT in line number %1.\n").
              arg(lineno);
              file.close();
              return false;
           }
        }

        int hi = -1; // signal an undefined number
        double tr =  zonerx.cap(5).toDouble();

        if (zones_are_defaults) {
           scheme.nzones_default ++;
           scheme.zone_default_is_pct.append(lo_is_pct);
           scheme.zone_default.append(lo);
           scheme.zone_default_name.append(zonerx.cap(1));
           scheme.zone_default_desc.append(zonerx.cap(2));
           scheme.zone_default_trimp.append(trimp);
           defaults_from_user = true;
        }
        else {
           HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr);
           zoneInfos.append(zone);
        }
    }
    next_line: {}
    }

    if (in_range) {
        HrZoneRange range(begin, end, lt, restHr, maxHr);
        range.zones = zoneInfos;
        if (range.zones.empty()) {
            if (range.lt > 0)
            setHrZonesFromLT(range);
        else {
                err = tr("file ended without reading any zones for last range");
                file.close();
                return false;
        }
        }
    else {
        qSort(range.zones);
    }

        ranges.append(range);
    }
    file.close();

    // sort the ranges
    qSort(ranges);

    // set the default zones if not in file
    if (!scheme.nzones_default)  {

        // do we have a zone which is explicitly set?
        for (int i=0; i<ranges.count(); i++) {
                if (ranges[i].hrZonesSetFromLT == false) {
                        // set the defaults using this one!
                        scheme.nzones_default = ranges[i].zones.count();
                        for (int j=0; j<scheme.nzones_default; j++) {
                                scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].lt) * 100.00);
                                scheme.zone_default_is_pct.append(true);
                                scheme.zone_default_name.append(ranges[i].zones[j].name);
                                scheme.zone_default_desc.append(ranges[i].zones[j].desc);
                                scheme.zone_default_trimp.append(ranges[i].zones[j].trimp);
                        }
                }
        }

        // still not set then reset to defaults as usual
        if (!scheme.nzones_default) initializeZoneParameters();
    }

    // resolve undefined endpoints in ranges and zones
    for (int nr = 0; nr < ranges.size(); nr ++) {
    // clean up gaps or overlaps in zone ranges
    if (ranges[nr].end.isNull())
        ranges[nr].end =
        (nr < ranges.size() - 1) ?
        ranges[nr + 1].begin :
        date_infinity;
    else if ((nr < ranges.size() - 1) &&
         (ranges[nr + 1].begin != ranges[nr].end)) {

        append_to_warning(tr("Setting end date of range %1 "
                 "to start date of range %2.\n").
                  arg(nr + 1).
                  arg(nr + 2)
                  );

        ranges[nr].end = ranges[nr + 1].begin;
    }
    else if ((nr == ranges.size() - 1) &&
         (ranges[nr].end < QDate::currentDate())) {

        append_to_warning(tr("Extending final range %1 to infinite "
                 "to include present date.\n").arg(nr + 1));

        ranges[nr].end = date_infinity;
    }

        if (ranges[nr].lt <= 0) {
            err = tr("LT must be greater than zero in zone "
                     "range %1 of hr.zones").arg(nr + 1);
            return false;
        }

    if (ranges[nr].zones.size()) {
        // check that the first zone starts with zero
        ranges[nr].zones[0].lo = 0;

        // resolve zone end powers
        for (int nz = 0; nz < ranges[nr].zones.size(); nz ++) {
        if (ranges[nr].zones[nz].hi == -1)
            ranges[nr].zones[nz].hi =
            (nz < ranges[nr].zones.size() - 1) ?
            ranges[nr].zones[nz + 1].lo :
            INT_MAX;
        else if ((nz < ranges[nr].zones.size() - 1) &&
             (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) {
            if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) {
            append_to_warning(tr("Range %1: matching top of zone %2 "
                         "(%3) to bottom of zone %4 (%5).\n").
                      arg(nr + 1).
                      arg(ranges[nr].zones[nz].name).
                      arg(ranges[nr].zones[nz].hi).
                      arg(ranges[nr].zones[nz + 1].name).
                      arg(ranges[nr].zones[nz + 1].lo)
                      );
                    }
            ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo;

        } else if ((nz == ranges[nr].zones.size() - 1) &&
             (ranges[nr].zones[nz].hi < INT_MAX)) {

            append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n").
                      arg(nr + 1).
                      arg(ranges[nr].zones[nz].name).
                      arg(ranges[nr].zones[nz].hi)
                      );
            ranges[nr].zones[nz].hi = INT_MAX;
        }
            }
    }
    }

    // mark zones as modified so pages which depend on zones can be updated
    modificationTime = QDateTime::currentDateTime();

    return true;
}

// note empty dates are treated as automatic matches for begin or
// end of range
int HrZones::whichRange(const QDate &date) const
{
    for (int rnum = 0; rnum < ranges.size(); ++rnum) {
        const HrZoneRange &range = ranges[rnum];
        if (((date >= range.begin) || (range.begin.isNull())) &&
            ((date < range.end) || (range.end.isNull())))
            return rnum;
    }
    return -1;
}

int HrZones::numZones(int rnum) const
{
    if (rnum < 0 || rnum >= ranges.size()) return 0;
    return ranges[rnum].zones.size();
}

int HrZones::whichZone(int rnum, double value) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    const HrZoneRange &range = ranges[rnum];
    for (int j = 0; j < range.zones.size(); ++j) {
        const HrZoneInfo &info = range.zones[j];
    // note: the "end" of range is actually in the next zone
        if ((value >= info.lo) && (value < info.hi))
            return j;
    }

    // if we got here either it is negative, nan, inf or way high
    if (value < 0 || isnan(value)) return 0;
    else return range.zones.size()-1;
}

void HrZones::zoneInfo(int rnum, int znum,
                     QString &name, QString &description,
                     int &low, int &high, double &trimp) const
{
    assert(rnum < ranges.size());
    const HrZoneRange &range = ranges[rnum];
    assert(znum < range.zones.size());
    const HrZoneInfo &zone = range.zones[znum];
    name = zone.name;
    description = zone.desc;
    low = zone.lo;
    high = zone.hi;
    trimp= zone.trimp;
}

int HrZones::getLT(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].lt;
}

void HrZones::setLT(int rnum, int lt)
{
    ranges[rnum].lt = lt;
    modificationTime = QDateTime::currentDateTime();
}

// generate a list of zones from LT
int HrZones::lowsFromLT(QList <int> *lows, int lt) const {

    lows->clear();

    for (int z = 0; z < scheme.nzones_default; z++)
    lows->append(scheme.zone_default_is_pct[z] ?
                scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]);

    return scheme.nzones_default;
}

int HrZones::getRestHr(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].restHr;
}

void HrZones::setRestHr(int rnum, int restHr)
{
    ranges[rnum].restHr = restHr;
    modificationTime = QDateTime::currentDateTime();
}

int HrZones::getMaxHr(int rnum) const
{
    if (rnum < 0 || rnum > ranges.size()) return 0;
    return ranges[rnum].maxHr;
}

void HrZones::setMaxHr(int rnum, int maxHr)
{
    ranges[rnum].maxHr = maxHr;
    modificationTime = QDateTime::currentDateTime();
}

// access the zone name
QString HrZones::getDefaultZoneName(int z) const {
    return scheme.zone_default_name[z];
}

// access the zone description
QString HrZones::getDefaultZoneDesc(int z) const {
    return scheme.zone_default_desc[z];
}

// set the zones from the LT value (the cp variable)
void HrZones::setHrZonesFromLT(HrZoneRange &range) {
    range.zones.clear();

    if (scheme.nzones_default == 0)
    initializeZoneParameters();

    for (int i = 0; i < scheme.nzones_default; i++) {
    int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i];
    int hi = lo;
    double trimp = scheme.zone_default_trimp[i];

    HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp);
    range.zones.append(zone);
    }

    // sort the zones (some may be pct, others absolute, so zones need to be sorted,
    // rather than the defaults
    qSort(range.zones);

    // set zone end dates
    for (int i = 0; i < range.zones.size(); i ++)
    range.zones[i].hi =
        (i < scheme.nzones_default - 1) ?
        range.zones[i + 1].lo :
        INT_MAX;

    // mark that the zones were set from LT, so if zones are subsequently
    // written, only LT is saved
    range.hrZonesSetFromLT = true;
}

void HrZones::setHrZonesFromLT(int rnum) {
    assert((rnum >= 0) && (rnum < ranges.size()));
    setHrZonesFromLT(ranges[rnum]);
}

// return the list of starting values of zones for a given range
QList <int> HrZones::getZoneLows(int rnum) const {
    if (rnum >= ranges.size())
        return QList <int>();
    const HrZoneRange &range = ranges[rnum];
    QList <int> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].lo);
    return return_values;
}

// return the list of ending values of zones for a given range
QList <int> HrZones::getZoneHighs(int rnum) const {
    if (rnum >= ranges.size())
        return QList <int>();
    const HrZoneRange &range = ranges[rnum];
    QList <int> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].hi);
    return return_values;
}

// return the list of zone names
QList <QString> HrZones::getZoneNames(int rnum) const {
    if (rnum >= ranges.size())
        return QList <QString>();
    const HrZoneRange &range = ranges[rnum];
    QList <QString> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].name);
    return return_values;
}

// return the list of zone trimp coef
QList <double> HrZones::getZoneTrimps(int rnum) const {
    if (rnum >= ranges.size())
        return QList <double>();
    const HrZoneRange &range = ranges[rnum];
    QList <double> return_values;
    for (int i = 0; i < range.zones.size(); i ++)
        return_values.append(ranges[rnum].zones[i].trimp);
    return return_values;
}

QString HrZones::summarize(int rnum, QVector<double> &time_in_zone) const
{
    assert(rnum < ranges.size());
    const HrZoneRange &range = ranges[rnum];
    if (time_in_zone.size() < range.zones.size()) return "";
    QString summary;
    if(range.lt > 0){
        summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
        summary += "<tr><td align=\"center\">";
        summary += tr("Threshold (bpm): %1").arg(range.lt);
        summary += "</td></tr></table>";
    }
    summary += "<table align=\"center\" width=\"70%\" ";
    summary += "border=\"0\">";
    summary += "<tr>";
    summary += tr("<td align=\"center\">Zone</td>");
    summary += tr("<td align=\"center\">Description</td>");
    summary += tr("<td align=\"center\">Low (bpm)</td>");
    summary += tr("<td align=\"center\">High (bpm)</td>");
    summary += tr("<td align=\"center\">Time</td>");
    summary += tr("<td align=\"center\">%</td>");
    summary += "</tr>";
    QColor color = QApplication::palette().alternateBase().color();
    color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value());

    double duration = 0;
    foreach(double v, time_in_zone) { duration += v; }

    for (int zone = 0; zone < time_in_zone.size(); ++zone) {
        if (time_in_zone[zone] > 0.0) {
            QString name, desc;
            int lo, hi;
            double trimp;
            zoneInfo(rnum, zone, name, desc, lo, hi, trimp);
            if (zone % 2 == 0)
                summary += "<tr bgcolor='" + color.name() + "'>";
            else
                summary += "<tr>";
            summary += QString("<td align=\"center\">%1</td>").arg(name);
            summary += QString("<td align=\"center\">%1</td>").arg(desc);
            summary += QString("<td align=\"center\">%1</td>").arg(lo);
            if (hi == INT_MAX)
                summary += "<td align=\"center\">MAX</td>";
            else
                summary += QString("<td align=\"center\">%1</td>").arg(hi);
            summary += QString("<td align=\"center\">%1</td>")
                .arg(time_to_string((unsigned) round(time_in_zone[zone])));
            summary += QString("<td align=\"center\">%1</td>")
                .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0);
            summary += "</tr>";
        }
    }
    summary += "</table>";
    return summary;
}

#define USE_SHORT_POWER_ZONES_FORMAT true   /* whether a less redundent format should be used */
void HrZones::write(QDir home)
{
    QString strzones;

    // always write the defaults (config pane can adjust)
    strzones += QString("DEFAULTS:\n");
    for (int z = 0 ; z < scheme.nzones_default; z ++)
        strzones += QString("%1,%2,%3%4,%5\n").
        arg(scheme.zone_default_name[z]).
        arg(scheme.zone_default_desc[z]).
        arg(scheme.zone_default[z]).
        arg(scheme.zone_default_is_pct[z]?"%":"").
        arg(scheme.zone_default_trimp[z]);
    strzones += QString("\n");

    for (int i = 0; i < ranges.size(); i++) {
        int lt = getLT(i);
        int restHr = getRestHr(i);
        int maxHr = getMaxHr(i);

    // print header for range
    // note this explicitly sets the first and last ranges such that all time is spanned

        // note: BEGIN is not needed anymore
        //       since it becomes Jan 01 1900
        strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr);
    strzones += QString("\n");

    // step through and print the zones if they've been explicitly set
    if (! ranges[i].hrZonesSetFromLT) {
        for (int j = 0; j < ranges[i].zones.size(); j ++) {
                const HrZoneInfo &zi = ranges[i].zones[j];
        strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp);
            }
        strzones += QString("\n");
    }
    }

    QFile file(home.absolutePath() + "/hr.zones");
    if (file.open(QFile::WriteOnly))
    {
        QTextStream stream(&file);
        stream << strzones;
        file.close();
    }
}
Пример #7
0
// insert a new range starting at the given date extending to the end of the zone currently
// containing that date.  If the start date of that zone is prior to the specified start
// date, then that zone range is shorted.
int Zones::insertRangeAtDate(QDate date, int cp) {
    assert(date.isValid());
    int rnum;

    if (ranges.empty()) {
	addZoneRange(cp);
	fprintf(
		stderr,
		"Generating first range with CP = %d\n",
		cp
		);
	rnum = 0;
    }
    
    else {
	rnum = whichRange(date);
	assert(rnum >= 0);
	QDate date1 = getStartDate(rnum);
	fprintf(stderr, "insertRangeAtDate(%s, %d):\n", date.toString().toAscii().constData(), cp);

	// if the old range has dates before the specified, then truncate the old range
	// and shift up the existing ranges
	if (date > date1) {
	    QDate endDate = getEndDate(rnum);
	    setEndDate(rnum, date);
	    fprintf(
		    stderr,
		    "Inserting range\n"
		    "old range %d: from %s to %s\n"
		    "new range %d: from %s to %s\n"
		    "added range %d: from %s to %s\n",
		    rnum + 1, getStartDateString(rnum).toAscii().constData(), getEndDateString(rnum).toAscii().constData(),
		    rnum + 1, getStartDateString(rnum).toAscii().constData(), (date.isNull() ? "END" : date.toString().toAscii().constData()), 
		    rnum + 2, (date.isNull() ? "BEGIN" : date.toString().toAscii().constData()), getEndDateString(rnum).toAscii().constData()
		    );
	    ranges.insert(++ rnum, new ZoneRange(date, endDate));
	}
    }

    if (cp > 0) {
	setCP(rnum, cp);
	setZonesFromCP(rnum);
    }

    return rnum;
}
Пример #8
0
float WriMoImp::getRunningAverageWordCountForDate(
		const Date& date) const
{
	if (!m_calendar.dateInRange(date))
	{
		throw DateOutOfRangeError();
	}

	unsigned int sum = 0;
	unsigned int numDays = 0;

	Date currentDate = getStartDate();
	do
	{
		sum += getWordCount(currentDate);
		numDays++;
		currentDate = currentDate.addDays(1);
	}
	while (currentDate <= date);

	return (float)sum / numDays;
}
Пример #9
0
/*
 * Copyright (c) 2006 Sean C. Rhea ([email protected])
 * Copyright (c) 2014 Mark Liversedge ([email protected])
 *
 * [mostly cut and paste from Zones.cpp]
 *
 * 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 <QMessageBox>
#include "PaceZones.h"
#include "Colors.h"
#include "Settings.h"
#include "TimeUtils.h"
#include "Units.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <math.h>


// the infinity endpoints are indicated with extreme date ranges
// but not zero dates so we can edit and compare them
static const QDate date_zero(1900, 01, 01);
static const QDate date_infinity(9999,12,31);

// initialize default static zone parameters
void PaceZones::initializeZoneParameters()
{
    // these default zones are based upon the Skiba pace zones 
    // but expressed as a percentage of Critical Velocity in km/h
    // rather than as a percentage of LT Pace in minutes/mile
    //
    // Name         %LT Pace         %CV
    // AR              > 125        0 - 80
    // Endurance    124 - 115      80 - 87
    // Tempo        114 - 105      88 - 95
    // Threshold    104 - 95       95 - 105
    // Vo2Max        94 - 84      105 - 119
    // Anaerobic       < 84         > 119
    //
    static int initial_zone_default[] = {
        0, 80, 87, 95, 105, 119
    };
    static const QString initial_zone_default_desc[] = {
        tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"),
        tr("VO2Max"), tr("Anaerobic")
    };
    static const char *initial_zone_default_name[] = {
        "Z1", "Z2", "Z3", "Z4", "Z5", "Z6"
    };

    static int initial_nzones_default =
        sizeof(initial_zone_default) /
        sizeof(initial_zone_default[0]);

    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_name.clear();
    scheme.nzones_default = 0;

    scheme.nzones_default = initial_nzones_default;

    for (int z = 0; z < scheme.nzones_default; z++) {
        scheme.zone_default.append(initial_zone_default[z]);
        scheme.zone_default_is_pct.append(true);
        scheme.zone_default_name.append(QString(initial_zone_default_name[z]));
        scheme.zone_default_desc.append(QString(initial_zone_default_desc[z]));
    }
}

// read zone file, allowing for zones with or without end dates
bool PaceZones::read(QFile &file)
{
    defaults_from_user = false;
    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_desc.clear();
    scheme.nzones_default = 0;
    ranges.clear();

    // set up possible warning dialog
    warning = QString();
    int warning_lines = 0;
    const int max_warning_lines = 100;

    // macro to append lines to the warning
    #define append_to_warning(s) \
    if (warning_lines < max_warning_lines) { \
        warning.append(s); }  \
    else if (warning_lines == max_warning_lines) { \
        warning.append("...\n"); } \
    warning_lines++;

    // read using text mode takes care of end-lines
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        err = "can't open file";
        return false;
    }
    QTextStream fileStream(&file);

    QRegExp commentrx("\\s*#.*$");
    QRegExp blankrx("^[ \t]*$");
    QRegExp rangerx[] = {
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s*([,:]?\\s*(FTP|CV)\\s*=\\s*([\\d\\.]+))?"            // optional {CV/FTP = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive),
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s+(?:until|to|-)\\s+"                             // until
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?"  // end date
                "\\s*:?,?\\s*((FTP|CV)\\s*=\\s*([\\d\\.]+))?"          // optional {CV/FTP = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive)
    };
    QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
                   "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$",
                   Qt::CaseInsensitive);
    QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
                          Qt::CaseInsensitive);

    int lineno = 0;

    // the current range in the file
    // PaceZoneRange *range = NULL;
    bool in_range = false;
    QDate begin = date_zero, end = date_infinity;
    double cv=0;
    QList<PaceZoneInfo> zoneInfos;

    // true if zone defaults are found in the file (then we need to write them)
    bool zones_are_defaults = false;

    while (!fileStream.atEnd() ) {
        ++lineno;
        QString line = fileStream.readLine();
        int pos = commentrx.indexIn(line, 0);
        if (pos != -1)
            line = line.left(pos); if (blankrx.indexIn(line, 0) == 0)
            goto next_line;
        // check for default zone range definition (may be followed by zone definitions)
        if (zonedefaultsx.indexIn(line, 0) != -1) {
            zones_are_defaults = true;

            // defaults are allowed only at the beginning of the file
            if (ranges.size()) {
                err = "Zone defaults must be specified at head of pace.zones file";
                return false;
            }

            // only one set of defaults is allowed
            if (scheme.nzones_default) {
                err = "Only one set of zone defaults may be specified in pace.zones file";
                return false;
            }

            goto next_line;
        }

        // check for range specification (may be followed by zone definitions)
        for (int r=0; r<2; r++) {

            if (rangerx[r].indexIn(line, 0) != -1) {

                if (in_range) {

                    // if zones are empty, then generate them
                    PaceZoneRange range(begin, end, cv);
                    range.zones = zoneInfos;

                    if (range.zones.empty()) {

                        if (range.cv > 0) { 

                            setZonesFromCV(range); 

                        } else {

                            err = tr("line %1: read new range without reading "
                                     "any zones for previous one").arg(lineno);
                            file.close();
                            return false;
                        }

                    } else {

                        qSort(range.zones);

                    }
                    ranges.append(range);
                }

                in_range = true;
                zones_are_defaults = false;
                zoneInfos.clear();

                // process the beginning date
                if (rangerx[r].cap(1) == "BEGIN") {

                    begin = date_zero; 

                } else {

                    begin = QDate(rangerx[r].cap(2).toInt(),
                                  rangerx[r].cap(3).toInt(),
                                  rangerx[r].cap(4).toInt());
                }

                // process an end date, if any, else it is null
                if (rangerx[r].cap(5) == "END") {

                    end = date_infinity; 

                } else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) {

                    end = QDate(rangerx[r].cap(6).toInt(),
                                rangerx[r].cap(7).toInt(),
                                rangerx[r].cap(8).toInt());

                } else {

                    end = QDate();
                }

                // set up the range, capturing CV if it's specified
                // range = new PaceZoneRange(begin, end);
                int nCV = (r ? 11 : 7);
                if (rangerx[r].captureCount() == (nCV)) cv = rangerx[r].cap(nCV).toDouble(); 
                else cv = 0;

                // bleck
                goto next_line;
            }
        }

        // check for zone definition
        if (zonerx.indexIn(line, 0) != -1) {

            if (!(in_range || zones_are_defaults)) {

                err = tr("line %1: read zone without "
                         "preceding date range").arg(lineno);
                file.close();
                return false;
            }

            double lo = zonerx.cap(3).toInt();

            // allow for zone specified as % of CV
            bool lo_is_pct = false;
            if (zonerx.cap(4) == "%") {

                if (zones_are_defaults) {

                    lo_is_pct = true;

                } else if (cv > 0)  {

                    lo = double(lo * cv / 100.00f);

                } else {

                    err = tr("attempt to set zone based on % of "
                             "CV without setting CV in line number %1.\n").arg(lineno);
                    file.close();
                    return false;
                }
            }

            double hi;
            // if this is not a zone defaults specification, process possible hi end of zones
            if (zones_are_defaults || zonerx.cap(5).isEmpty()) {

                hi = -1;   // signal an undefined number

            } else if (zonerx.cap(5) == "MAX") {

                hi = INT_MAX;

            } else {

                hi = zonerx.cap(5).toInt();

                // allow for zone specified as % of CV
                if (zonerx.cap(5) == "%") {

                    if (cv > 0) {

                        hi = double(hi * cv / 100.00f);

                    } else {

                        err = tr("attempt to set zone based on % of CV "
                                 "without setting CV in line number %1.\n").
                              arg(lineno);
                        file.close();
                        return false;

                    }
                }
            }

            if (zones_are_defaults) {

                scheme.nzones_default++;
                scheme.zone_default_is_pct.append(lo_is_pct);
                scheme.zone_default.append(lo);
                scheme.zone_default_name.append(zonerx.cap(1));
                scheme.zone_default_desc.append(zonerx.cap(2));
                defaults_from_user = true;

            } else {

                PaceZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi);
                zoneInfos.append(zone);
            }
        }
next_line: {}
    }

    if (in_range) {

        PaceZoneRange range(begin, end, cv);
        range.zones = zoneInfos;

        if (range.zones.empty()) {

            if (range.cv > 0) {

                setZonesFromCV(range);

            } else {

                err = tr("file ended without reading any zones for last range");
                file.close();
                return false;
            }

        } else {

            qSort(range.zones);
        }

        ranges.append(range);
    }
    file.close();

    // sort the ranges
    qSort(ranges);

    // set the default zones if not in file
    if (!scheme.nzones_default)  {

        // do we have a zone which is explicitly set?
        for (int i=0; i<ranges.count(); i++) {

            if (ranges[i].zonesSetFromCV == false) {

                // set the defaults using this one!
                scheme.nzones_default = ranges[i].zones.count();
                for (int j=0; j<scheme.nzones_default; j++) {

                    scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].cv) * 100.00);
                    scheme.zone_default_is_pct.append(true);
                    scheme.zone_default_name.append(ranges[i].zones[j].name);
                    scheme.zone_default_desc.append(ranges[i].zones[j].desc);
                }
            }
        }

        // still not set then reset to defaults as usual
        if (!scheme.nzones_default) initializeZoneParameters();
    }

    // resolve undefined endpoints in ranges and zones
    for (int nr = 0; nr < ranges.size(); nr++) {

        // clean up gaps or overlaps in zone ranges
        if (ranges[nr].end.isNull()) {
            ranges[nr].end =
                (nr < ranges.size() - 1) ?
                ranges[nr + 1].begin :
                date_infinity;

        } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) {

            append_to_warning(tr("Setting end date of range %1 "
                                 "to start date of range %2.\n").
                              arg(nr + 1).
                              arg(nr + 2)
                              );

            ranges[nr].end = ranges[nr + 1].begin;

        } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) {

            append_to_warning(tr("Extending final range %1 to infinite "
                                 "to include present date.\n").arg(nr + 1));
            ranges[nr].end = date_infinity;
        }

        if (ranges[nr].cv <= 0) {

            err = tr("CV must be greater than zero in zone "
                     "range %1 of pace.zones").arg(nr + 1);
            return false;
        }

        if (ranges[nr].zones.size()) {

            // check that the first zone starts with zero
            // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this

            // resolve zone end powers
            for (int nz = 0; nz < ranges[nr].zones.size(); nz++) {

                if (ranges[nr].zones[nz].hi == -1) {
                    ranges[nr].zones[nz].hi =
                        (nz < ranges[nr].zones.size() - 1) ?
                        ranges[nr].zones[nz + 1].lo :
                        INT_MAX;

                } else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) {

                    if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) {

                        append_to_warning(tr("Range %1: matching top of zone %2 "
                                             "(%3) to bottom of zone %4 (%5).\n").
                                          arg(nr + 1).
                                          arg(ranges[nr].zones[nz].name).
                                          arg(ranges[nr].zones[nz].hi).
                                          arg(ranges[nr].zones[nz + 1].name).
                                          arg(ranges[nr].zones[nz + 1].lo)
                                          );

                    }
                    ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo;

                } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) {

                    append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n").
                                      arg(nr + 1).
                                      arg(ranges[nr].zones[nz].name).
                                      arg(ranges[nr].zones[nz].hi)
                                      );
                    ranges[nr].zones[nz].hi = INT_MAX;
                }
            }
        }
    }

    // mark zones as modified so pages which depend on zones can be updated
    modificationTime = QDateTime::currentDateTime();

    return true;
}

// note empty dates are treated as automatic matches for begin or
// end of range
int PaceZones::whichRange(const QDate &date) const
{
    for (int rnum = 0; rnum < ranges.size(); ++rnum) {

        const PaceZoneRange &range = ranges[rnum];
        if (((date >= range.begin) || (range.begin.isNull())) &&
            ((date < range.end) || (range.end.isNull())))
            return rnum;

    }
    return -1;
}

int PaceZones::numZones(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].zones.size();
}

int PaceZones::whichZone(int rnum, double value) const
{
    assert(rnum < ranges.size());
    const PaceZoneRange &range = ranges[rnum];

    for (int j = 0; j < range.zones.size(); ++j) {

        const PaceZoneInfo &info = range.zones[j];

        // note: the "end" of range is actually in the next zone
        if ((value >= info.lo) && (value < info.hi))
            return j;

    }

    // if we got here either it is negative, nan, inf or way high
    if (value < 0 || isnan(value)) return 0; else return range.zones.size()-1;
}

void PaceZones::zoneInfo(int rnum, int znum, QString &name, QString &description, double &low, double &high) const
{
    assert(rnum < ranges.size());
    const PaceZoneRange &range = ranges[rnum];
    assert(znum < range.zones.size());
    const PaceZoneInfo &zone = range.zones[znum];
    name = zone.name;
    description = zone.desc;
    low = zone.lo;
    high = zone.hi;
}

double PaceZones::getCV(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].cv;
}

void PaceZones::setCV(int rnum, double cv)
{
    ranges[rnum].cv = cv;
    modificationTime = QDateTime::currentDateTime();
}

// generate a list of zones from CV
int PaceZones::lowsFromCV(QList <double> *lows, double cv) const {
    lows->clear();

    for (int z = 0; z < scheme.nzones_default; z++) {

        lows->append(scheme.zone_default_is_pct[z] ?  scheme.zone_default[z] * cv / 100.00f : scheme.zone_default[z]);

    }

    return scheme.nzones_default;
}

// access the zone name
QString PaceZones::getDefaultZoneName(int z) const {
    return scheme.zone_default_name[z];
}

// access the zone description
QString PaceZones::getDefaultZoneDesc(int z) const {
    return scheme.zone_default_desc[z];
}

// set the zones from the CV value (the cv variable)
void PaceZones::setZonesFromCV(PaceZoneRange &range)
{
    range.zones.clear();

    if (scheme.nzones_default == 0) initializeZoneParameters();

    for (int i = 0; i < scheme.nzones_default; i++) {

        double lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.cv / 100.00f : scheme.zone_default[i];
        double hi = lo;
        PaceZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi);
        range.zones.append(zone);
    }

    // sort the zones (some may be pct, others absolute, so zones need to be sorted,
    // rather than the defaults
    qSort(range.zones);

    // set zone end dates
    for (int i = 0; i < range.zones.size(); i++) {

        range.zones[i].hi =
            (i < scheme.nzones_default - 1) ?
            range.zones[i + 1].lo :
            INT_MAX;
    }

    // mark that the zones were set from CV, so if zones are subsequently
    // written, only CV is saved
    range.zonesSetFromCV = true;
}

void PaceZones::setZonesFromCV(int rnum)
{
    assert((rnum >= 0) && (rnum < ranges.size()));
    setZonesFromCV(ranges[rnum]);
}

// return the list of starting values of zones for a given range
QList <double> PaceZones::getZoneLows(int rnum) const
{
    if (rnum >= ranges.size()) return QList <double>();

    const PaceZoneRange &range = ranges[rnum];
    QList <double> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].lo);
    }

    return return_values;
}

// return the list of ending values of zones for a given range
QList <double> PaceZones::getZoneHighs(int rnum) const
{

    if (rnum >= ranges.size()) return QList <double>();

    const PaceZoneRange &range = ranges[rnum];
    QList <double> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].hi);
    }

    return return_values;
}

// return the list of zone names
QList <QString> PaceZones::getZoneNames(int rnum) const
{
    if (rnum >= ranges.size()) return QList <QString>(); 

    const PaceZoneRange &range = ranges[rnum];
    QList <QString> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].name);
    }

    return return_values;
}

QString PaceZones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const
{

    assert(rnum < ranges.size());
    const PaceZoneRange &range = ranges[rnum];
    if (time_in_zone.size() != range.zones.size()) time_in_zone.resize(range.zones.size());

    // are we in metric or imperial ?
    bool metric = appsettings->value(this, GC_PACE, true).toBool();
    QString cvunit = metric ? "kph" : "mph";
    QString paceunit = metric ? "min/km" : "min/mile";
    double cvfactor = metric ? 1.0f : KM_PER_MILE;

    QString summary;
    if(range.cv > 0) {
        summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
        summary += "<tr><td align=\"center\">";
        summary += tr("Critical Velocity: %3%4 (%2%1)").arg(cvunit).arg(range.cv / cvfactor, 0, 'f', 2)
                                                        .arg(kphToPace(range.cv, metric))
                                                        .arg(paceunit);
        summary += "</td></tr></table>";
    }
    summary += "<table align=\"center\" width=\"70%\" ";
    summary += "border=\"0\">";
    summary += "<tr>";
    summary += tr("<td align=\"center\">Zone</td>");
    summary += tr("<td align=\"center\">Description</td>");
    summary += tr("<td align=\"center\">Low (%1)</td>").arg(paceunit);
    summary += tr("<td align=\"center\">High (%1)</td>").arg(paceunit);
    summary += tr("<td align=\"center\">Time</td>");
    summary += tr("<td align=\"center\">%</td>");
    summary += "</tr>";

    double duration = 0;
    foreach(double v, time_in_zone) {
        duration += v;
    }

    for (int zone = 0; zone < time_in_zone.size(); ++zone) {

        if (time_in_zone[zone] > 0.0) {
            QString name, desc;
            double lo, hi;
            zoneInfo(rnum, zone, name, desc, lo, hi);
            if (zone % 2 == 0)
                summary += "<tr bgcolor='" + color.name() + "'>"; else
                summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name);
            summary += QString("<td align=\"center\">%1</td>").arg(desc);
            summary += QString("<td align=\"center\">%1</td>").arg(kphToPace(lo, metric));
            if (hi == INT_MAX)
                summary += "<td align=\"center\">MAX</td>"; else
                summary += QString("<td align=\"center\">%1</td>").arg(kphToPace(hi, metric));
                summary += QString("<td align=\"center\">%1</td>").arg(time_to_string((unsigned) round(time_in_zone[zone])));
            summary += QString("<td align=\"center\">%1</td>")
                       .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0);
            summary += "</tr>";
        }
    }
    summary += "</table>";
    return summary;
}

#define USE_SHORT_POWER_ZONES_FORMAT true   /* whether a less redundent format should be used */
void PaceZones::write(QDir home)
{
    QString strzones;

    // always write the defaults (config pane can adjust)
    strzones += QString("DEFAULTS:\n");

    for (int z = 0; z < scheme.nzones_default; z++) {

        strzones += QString("%1,%2,%3%4\n").
                    arg(scheme.zone_default_name[z]).
                    arg(scheme.zone_default_desc[z]).
                    arg(scheme.zone_default[z]).
                    arg(scheme.zone_default_is_pct[z] ? "%" : "");

    }
    strzones += QString("\n");

    for (int i = 0; i < ranges.size(); i++) {

        double cv = getCV(i);

        // print header for range
        // note this explicitly sets the first and last ranges such that all time is spanned

#if USE_SHORT_POWER_ZONES_FORMAT

        // note: BEGIN is not needed anymore
        //       since it becomes Jan 01 1900
        strzones += QString("%1: CV=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cv);
        strzones += QString("\n");

        // step through and print the zones if they've been explicitly set
        if (!ranges[i].zonesSetFromCV) {
            for (int j = 0; j < ranges[i].zones.size(); j++) {
                const PaceZoneInfo &zi = ranges[i].zones[j];
                strzones += QString("%1,%2,%3\n").arg(zi.name).arg(zi.desc).arg(zi.lo);
            }
            strzones += QString("\n");
        }
#else
        if(ranges.size() <= 1) {

            strzones += QString("FROM BEGIN UNTIL END, CV=%1:").arg(cv);

        } else if (i == 0) {

            strzones += QString("FROM BEGIN UNTIL %1, CV=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cv);

        } else if (i == ranges.size() - 1) {

            strzones += QString("FROM %1 UNTIL END, CV=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cv);
        } else {
            strzones += QString("FROM %1 UNTIL %2, CV=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cv);
        }

        strzones += QString("\n");
        for (int j = 0; j < ranges[i].zones.size(); j++) {

            const PaceZoneInfo &zi = ranges[i].zones[j];

            if (ranges[i].zones[j].hi == INT_MAX) {
                strzones += QString("%1,%2,%3,MAX\n").arg(zi.name).arg(zi.desc).arg(zi.lo);

            } else {

                strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.hi);
            }
        }
        strzones += QString("\n");
#endif
    }

    QFile file(home.canonicalPath() + "/pace.zones");
    if (file.open(QFile::WriteOnly)) {

        QTextStream stream(&file);
        stream << strzones;
        file.close();
    }
}
Пример #10
0
/*
 * Copyright (c) 2006 Sean C. Rhea ([email protected])
 *
 * 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 <QMessageBox>
#include "Zones.h"
#include "Colors.h"
#include "Settings.h"
#include "TimeUtils.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <cmath>


// the infinity endpoints are indicated with extreme date ranges
// but not zero dates so we can edit and compare them
static const QDate date_zero(1900, 01, 01);
static const QDate date_infinity(9999,12,31);

// initialize default static zone parameters
void Zones::initializeZoneParameters()
{
    static int initial_zone_default[] = {
        0, 55, 75, 90, 105, 120, 150
    };
    static const QString initial_zone_default_desc[] = {
        tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"),
        tr("VO2Max"), tr("Anaerobic"), tr("Neuromuscular")
    };
    static const char *initial_zone_default_name[] = {
        "Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7"
    };

    static int initial_nzones_default =
        sizeof(initial_zone_default) /
        sizeof(initial_zone_default[0]);

    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_desc.clear();
    scheme.zone_default_name.clear();
    scheme.nzones_default = 0;

    scheme.nzones_default = initial_nzones_default;

    for (int z = 0; z < scheme.nzones_default; z++) {
        scheme.zone_default.append(initial_zone_default[z]);
        scheme.zone_default_is_pct.append(true);
        scheme.zone_default_name.append(QString(initial_zone_default_name[z]));
        scheme.zone_default_desc.append(QString(initial_zone_default_desc[z]));
    }
}

// read zone file, allowing for zones with or without end dates
bool Zones::read(QFile &file)
{
    defaults_from_user = false;
    scheme.zone_default.clear();
    scheme.zone_default_is_pct.clear();
    scheme.zone_default_name.clear();
    scheme.zone_default_desc.clear();
    scheme.nzones_default = 0;
    ranges.clear();

    // set up possible warning dialog
    warning = QString();
    int warning_lines = 0;
    const int max_warning_lines = 100;
    int defaultwprime = 20000; // default to 20kJ
    int defaultpmax = 1000;

    // macro to append lines to the warning
    #define append_to_warning(s) \
    if (warning_lines < max_warning_lines) { \
        warning.append(s); }  \
    else if (warning_lines == max_warning_lines) { \
        warning.append("...\n"); } \
    warning_lines++;

    // read using text mode takes care of end-lines
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        err = "can't open file";
        return false;
    }
    QTextStream fileStream(&file);

    QRegExp commentrx("\\s*#.*$");
    QRegExp blankrx("^[ \t]*$");
    QRegExp rangerx[] = {
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s*([,:]?\\s*(FTP|CP)\\s*=\\s*(\\d+))?"            // optional {CP/FTP = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive),
        QRegExp("^\\s*(?:from\\s+)?"                                 // optional "from"
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
                "\\s+(?:until|to|-)\\s+"                             // until
                "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?"  // end date
                "\\s*:?,?\\s*((FTP|CP)\\s*=\\s*(\\d+))?"          // optional {CP/FTP = integer (optional %)}
                "\\s*:?\\s*$",                                       // optional :
                Qt::CaseInsensitive)
    };
    QRegExp ftpx("^FTP=(\\d+)$");
    QRegExp wprimerx("^W'=(\\d+)$");
    QRegExp pmaxx("^Pmax=(\\d+)$");
    QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
                   "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$",
                   Qt::CaseInsensitive);
    QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
                          Qt::CaseInsensitive);

    int lineno = 0;

    // the current range in the file
    // ZoneRange *range = NULL;
    bool in_range = false;
    QDate begin = date_zero, end = date_infinity;
    int cp=0;
    int ftp=0;
    int wprime=0;
    int pmax=0;
    QList<ZoneInfo> zoneInfos;

    // true if zone defaults are found in the file (then we need to write them)
    bool zones_are_defaults = false;

    while (!fileStream.atEnd() ) {
        ++lineno;
        QString line = fileStream.readLine();
        int pos = commentrx.indexIn(line, 0);
        if (pos != -1)
            line = line.left(pos); if (blankrx.indexIn(line, 0) == 0)
            goto next_line;
        // check for default zone range definition (may be followed by zone definitions)
        if (zonedefaultsx.indexIn(line, 0) != -1) {
            zones_are_defaults = true;

            // defaults are allowed only at the beginning of the file
            if (ranges.size()) {
                err = "Zone defaults must be specified at head of power.zones file";
                return false;
            }

            // only one set of defaults is allowed
            if (scheme.nzones_default) {
                err = "Only one set of zone defaults may be specified in power.zones file";
                return false;
            }

            goto next_line;
        }

        // check for range specification (may be followed by zone definitions)
        for (int r=0; r<2; r++) {

            if (rangerx[r].indexIn(line, 0) != -1) {

                if (in_range) {

                    // if zones are empty, then generate them
                    ZoneRange range(begin, end, cp, ftp ? ftp : cp, wprime ? wprime : defaultwprime, pmax ? pmax : defaultpmax);
                    range.zones = zoneInfos;

                    if (range.zones.empty()) {

                        if (range.cp > 0) { 

                            setZonesFromCP(range); 

                        } else {

                            err = tr("line %1: read new range without reading "
                                     "any zones for previous one").arg(lineno);
                            file.close();
                            return false;
                        }

                    } else {

                        qSort(range.zones);

                    }
                    ranges.append(range);
                }

                in_range = true;
                zones_are_defaults = false;
                zoneInfos.clear();

                // process the beginning date
                if (rangerx[r].cap(1) == "BEGIN") {

                    begin = date_zero; 

                } else {

                    begin = QDate(rangerx[r].cap(2).toInt(),
                                  rangerx[r].cap(3).toInt(),
                                  rangerx[r].cap(4).toInt());
                }

                // process an end date, if any, else it is null
                if (rangerx[r].cap(5) == "END") {

                    end = date_infinity; 

                } else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) {

                    end = QDate(rangerx[r].cap(6).toInt(),
                                rangerx[r].cap(7).toInt(),
                                rangerx[r].cap(8).toInt());

                } else {

                    end = QDate();
                }

                // set up the range, capturing CP if it's specified
                // range = new ZoneRange(begin, end);
                int nCP = (r ? 11 : 7);
                if (rangerx[r].captureCount() == (nCP)) cp = rangerx[r].cap(nCP).toInt(); 
                else cp = 0;

                // bleck
                goto next_line;
            }
        }

        // check for FTP
        if (ftpx.indexIn(line, 0) != -1) {
            if (!in_range)
                qDebug()<<"ignoring errant FTP= in power.zones";
            else {
                ftp = ftpx.cap(1).toInt();

                // ok its stored, so if it is in kJ upscale
                // if it is zero as never set, then use default
                if (!ftp) {
                    ftp = cp;
                }
            }
        }

        // check for w'
        if (wprimerx.indexIn(line, 0) != -1) {
            if (!in_range)
                qDebug()<<"ignoring errant W'= in power.zones";
            else {
                wprime = wprimerx.cap(1).toInt();

                // ok its stored, so if it is in kJ upscale
                // if it is zero as never set, then use default
                if (wprime) {
                    if (wprime < 1000) wprime *= 1000; // JOULES not kJ
                    defaultwprime = wprime;
                } else {
                    wprime = defaultwprime;
                }
            }
        }

        // check for Pmax
        if (pmaxx.indexIn(line, 0) != -1) {
            if (!in_range)
                qDebug()<<"ignoring errant Pmax= in power.zones";
            else {
                pmax = pmaxx.cap(1).toInt();
            }
        }


        // check for zone definition
        if (zonerx.indexIn(line, 0) != -1) {

            if (!(in_range || zones_are_defaults)) {

                err = tr("line %1: read zone without "
                         "preceding date range").arg(lineno);
                file.close();
                return false;
            }

            int lo = zonerx.cap(3).toInt();

            // allow for zone specified as % of CP
            bool lo_is_pct = false;
            if (zonerx.cap(4) == "%") {

                if (zones_are_defaults) {

                    lo_is_pct = true;

                } else if (cp > 0)  {

                    lo = int(lo * cp / 100);

                } else {

                    err = tr("attempt to set zone based on % of "
                             "CP without setting CP in line number %1.\n").arg(lineno);
                    file.close();
                    return false;
                }
            }

            int hi;
            // if this is not a zone defaults specification, process possible hi end of zones
            if (zones_are_defaults || zonerx.cap(5).isEmpty()) {

                hi = -1;   // signal an undefined number

            } else if (zonerx.cap(5) == "MAX") {

                hi = INT_MAX;

            } else {

                hi = zonerx.cap(5).toInt();

                // allow for zone specified as % of CP
                if (zonerx.cap(5) == "%") {

                    if (cp > 0) {

                        hi = int(hi * cp / 100);

                    } else {

                        err = tr("attempt to set zone based on % of CP "
                                 "without setting CP in line number %1.\n").
                              arg(lineno);
                        file.close();
                        return false;

                    }
                }
            }

            if (zones_are_defaults) {

                scheme.nzones_default++;
                scheme.zone_default_is_pct.append(lo_is_pct);
                scheme.zone_default.append(lo);
                scheme.zone_default_name.append(zonerx.cap(1));
                scheme.zone_default_desc.append(zonerx.cap(2));
                defaults_from_user = true;

            } else {

                ZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi);
                zoneInfos.append(zone);
            }
        }
next_line: {}
    }

    if (in_range) {

        ZoneRange range(begin, end, cp, ftp ? ftp : cp, wprime ? wprime : defaultwprime, pmax ? pmax : defaultpmax);
        range.zones = zoneInfos;

        if (range.zones.empty()) {

            if (range.cp > 0) {

                setZonesFromCP(range);

            } else {

                err = tr("file ended without reading any zones for last range");
                file.close();
                return false;
            }

        } else {

            qSort(range.zones);
        }

        ranges.append(range);
    }
    file.close();

    // sort the ranges
    qSort(ranges);

    // set the default zones if not in file
    if (!scheme.nzones_default)  {

        // do we have a zone which is explicitly set?
        for (int i=0; i<ranges.count(); i++) {

            if (ranges[i].zonesSetFromCP == false) {

                // set the defaults using this one!
                scheme.nzones_default = ranges[i].zones.count();
                for (int j=0; j<scheme.nzones_default; j++) {

                    scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].cp) * 100.00);
                    scheme.zone_default_is_pct.append(true);
                    scheme.zone_default_name.append(ranges[i].zones[j].name);
                    scheme.zone_default_desc.append(ranges[i].zones[j].desc);
                }
            }
        }

        // still not set then reset to defaults as usual
        if (!scheme.nzones_default) initializeZoneParameters();
    }

    // resolve undefined endpoints in ranges and zones
    for (int nr = 0; nr < ranges.size(); nr++) {

        // clean up gaps or overlaps in zone ranges
        if (ranges[nr].end.isNull()) {
            ranges[nr].end =
                (nr < ranges.size() - 1) ?
                ranges[nr + 1].begin :
                date_infinity;

        } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) {

            append_to_warning(tr("Setting end date of range %1 "
                                 "to start date of range %2.\n").
                              arg(nr + 1).
                              arg(nr + 2)
                              );

            ranges[nr].end = ranges[nr + 1].begin;

        } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) {

            append_to_warning(tr("Extending final range %1 to infinite "
                                 "to include present date.\n").arg(nr + 1));
            ranges[nr].end = date_infinity;
        }

        if (ranges[nr].cp <= 0) {

            err = tr("CP must be greater than zero in zone "
                     "range %1 of power.zones").arg(nr + 1);
            return false;
        }

        if (ranges[nr].zones.size()) {

            // check that the first zone starts with zero
            // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this

            // resolve zone end powers
            for (int nz = 0; nz < ranges[nr].zones.size(); nz++) {

                if (ranges[nr].zones[nz].hi == -1) {
                    ranges[nr].zones[nz].hi =
                        (nz < ranges[nr].zones.size() - 1) ?
                        ranges[nr].zones[nz + 1].lo :
                        INT_MAX;

                } else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) {

                    if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) {

                        append_to_warning(tr("Range %1: matching top of zone %2 "
                                             "(%3) to bottom of zone %4 (%5).\n").
                                          arg(nr + 1).
                                          arg(ranges[nr].zones[nz].name).
                                          arg(ranges[nr].zones[nz].hi).
                                          arg(ranges[nr].zones[nz + 1].name).
                                          arg(ranges[nr].zones[nz + 1].lo)
                                          );

                    }
                    ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo;

                } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) {

                    append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n").
                                      arg(nr + 1).
                                      arg(ranges[nr].zones[nz].name).
                                      arg(ranges[nr].zones[nz].hi)
                                      );
                    ranges[nr].zones[nz].hi = INT_MAX;
                }
            }
        }
    }

    // mark zones as modified so pages which depend on zones can be updated
    modificationTime = QDateTime::currentDateTime();

    return true;
}

// note empty dates are treated as automatic matches for begin or
// end of range
int Zones::whichRange(const QDate &date) const
{
    for (int rnum = 0; rnum < ranges.size(); ++rnum) {

        const ZoneRange &range = ranges[rnum];
        if (((date >= range.begin) || (range.begin.isNull())) &&
            ((date < range.end) || (range.end.isNull())))
            return rnum;

    }
    return -1;
}

int Zones::numZones(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].zones.size();
}

int Zones::whichZone(int rnum, double value) const
{
    assert(rnum < ranges.size());
    const ZoneRange &range = ranges[rnum];

    for (int j = 0; j < range.zones.size(); ++j) {

        const ZoneInfo &info = range.zones[j];

        // note: the "end" of range is actually in the next zone
        if ((value >= info.lo) && (value < info.hi))
            return j;

    }

    // if we got here either it is negative, nan, inf or way high
    return -1;
}

void Zones::zoneInfo(int rnum, int znum, QString &name, QString &description, int &low, int &high) const
{
    assert(rnum < ranges.size());
    const ZoneRange &range = ranges[rnum];
    assert(znum < range.zones.size());
    const ZoneInfo &zone = range.zones[znum];
    name = zone.name;
    description = zone.desc;
    low = zone.lo;
    high = zone.hi;
}

int Zones::getCP(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].cp;
}

int Zones::getFTP(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].ftp;
}

int Zones::getWprime(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].wprime;
}

int Zones::getPmax(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum].pmax;
}

void Zones::setCP(int rnum, int cp)
{
    ranges[rnum].cp = cp;
    modificationTime = QDateTime::currentDateTime();
}

void Zones::setFTP(int rnum, int ftp)
{
    ranges[rnum].ftp = ftp;
    modificationTime = QDateTime::currentDateTime();
}

void Zones::setWprime(int rnum, int wprime)
{
    ranges[rnum].wprime = wprime;
    modificationTime = QDateTime::currentDateTime();
}

void Zones::setPmax(int rnum, int pmax)
{
    ranges[rnum].pmax = pmax;
    modificationTime = QDateTime::currentDateTime();
}

// generate a list of zones from CP
int Zones::lowsFromCP(QList <int> *lows, int cp) const {
    lows->clear();

    for (int z = 0; z < scheme.nzones_default; z++) {

        lows->append(scheme.zone_default_is_pct[z] ?  scheme.zone_default[z] * cp / 100 : scheme.zone_default[z]);

    }

    return scheme.nzones_default;
}

// access the zone name
QString Zones::getDefaultZoneName(int z) const {
    return scheme.zone_default_name[z];
}

// access the zone description
QString Zones::getDefaultZoneDesc(int z) const {
    return scheme.zone_default_desc[z];
}

// set the zones from the CP value (the cp variable)
void Zones::setZonesFromCP(ZoneRange &range)
{
    range.zones.clear();

    if (scheme.nzones_default == 0) initializeZoneParameters();

    for (int i = 0; i < scheme.nzones_default; i++) {

        int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.cp / 100 : scheme.zone_default[i];
        int hi = lo;
        ZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi);
        range.zones.append(zone);
    }

    // sort the zones (some may be pct, others absolute, so zones need to be sorted,
    // rather than the defaults
    qSort(range.zones);

    // set zone end dates
    for (int i = 0; i < range.zones.size(); i++) {

        range.zones[i].hi =
            (i < scheme.nzones_default - 1) ?
            range.zones[i + 1].lo :
            INT_MAX;
    }

    // mark that the zones were set from CP, so if zones are subsequently
    // written, only CP is saved
    range.zonesSetFromCP = true;
}

void Zones::setZonesFromCP(int rnum)
{
    assert((rnum >= 0) && (rnum < ranges.size()));
    setZonesFromCP(ranges[rnum]);
}

// return the list of starting values of zones for a given range
QList <int> Zones::getZoneLows(int rnum) const
{
    if (rnum >= ranges.size()) return QList <int>();

    const ZoneRange &range = ranges[rnum];
    QList <int> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].lo);
    }

    return return_values;
}

// return the list of ending values of zones for a given range
QList <int> Zones::getZoneHighs(int rnum) const
{

    if (rnum >= ranges.size()) return QList <int>();

    const ZoneRange &range = ranges[rnum];
    QList <int> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].hi);
    }

    return return_values;
}

// return the list of zone names
QList <QString> Zones::getZoneNames(int rnum) const
{
    if (rnum >= ranges.size()) return QList <QString>(); 

    const ZoneRange &range = ranges[rnum];
    QList <QString> return_values;

    for (int i = 0; i < range.zones.size(); i++) {
        return_values.append(ranges[rnum].zones[i].name);
    }

    return return_values;
}

QString Zones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const
{
    assert(rnum < ranges.size());
    const ZoneRange &range = ranges[rnum];
    assert(time_in_zone.size() == range.zones.size());
    QString summary;
    if(range.cp > 0) {
        summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
        summary += "<tr><td align=\"center\">";
        summary += tr("Critical Power (watts): %1").arg(range.cp);
        summary += "</td></tr></table>";
    }
    summary += "<table align=\"center\" width=\"70%\" ";
    summary += "border=\"0\">";
    summary += "<tr>";
    summary += tr("<td align=\"center\">Zone</td>");
    summary += tr("<td align=\"center\">Description</td>");
    summary += tr("<td align=\"center\">Low (watts)</td>");
    summary += tr("<td align=\"center\">High (watts)</td>");
    summary += tr("<td align=\"center\">Time</td>");
    summary += tr("<td align=\"center\">%</td>");
    summary += "</tr>";

    double duration = 0;
    foreach(double v, time_in_zone) {
        duration += v;
    }

    for (int zone = 0; zone < time_in_zone.size(); ++zone) {
        if (time_in_zone[zone] > 0.0) {
            QString name, desc;
            int lo, hi;
            zoneInfo(rnum, zone, name, desc, lo, hi);
            if (zone % 2 == 0)
                summary += "<tr bgcolor='" + color.name() + "'>"; else
                summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name);
            summary += QString("<td align=\"center\">%1</td>").arg(desc);
            summary += QString("<td align=\"center\">%1</td>").arg(lo);
            if (hi == INT_MAX)
                summary += "<td align=\"center\">MAX</td>"; else
                summary += QString("<td align=\"center\">%1</td>").arg(hi); summary += QString("<td align=\"center\">%1</td>")
                                                                                       .arg(time_to_string((unsigned) round(time_in_zone[zone])));
            summary += QString("<td align=\"center\">%1</td>")
                       .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0);
            summary += "</tr>";
        }
    }
    summary += "</table>";
    return summary;
}

#define USE_SHORT_POWER_ZONES_FORMAT true   /* whether a less redundent format should be used */
void Zones::write(QDir home)
{
    QString strzones;

    // always write the defaults (config pane can adjust)
    strzones += QString("DEFAULTS:\n");

    for (int z = 0; z < scheme.nzones_default; z++) {

        strzones += QString("%1,%2,%3%4\n").
                    arg(scheme.zone_default_name[z]).
                    arg(scheme.zone_default_desc[z]).
                    arg(scheme.zone_default[z]).
                    arg(scheme.zone_default_is_pct[z] ? "%" : "");

    }
    strzones += QString("\n");

    for (int i = 0; i < ranges.size(); i++) {

        int cp = getCP(i);
        int ftp = getFTP(i);
        int wprime = getWprime(i);
        int pmax = getPmax(i);

        // print header for range
        // note this explicitly sets the first and last ranges such that all time is spanned

#if USE_SHORT_POWER_ZONES_FORMAT

        // note: BEGIN is not needed anymore
        //       since it becomes Jan 01 1900
        strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp);
        strzones += QString("\n");

        // wite out the FTP value
        strzones += QString("FTP=%1\n").arg(ftp);
        // wite out the W' value
        strzones += QString("W'=%1\n").arg(wprime);
        // wite out the Pmax value
        strzones += QString("Pmax=%1\n").arg(pmax);

        // step through and print the zones if they've been explicitly set
        if (!ranges[i].zonesSetFromCP) {
            for (int j = 0; j < ranges[i].zones.size(); j++) {
                const ZoneInfo &zi = ranges[i].zones[j];
                strzones += QString("%1,%2,%3\n").arg(zi.name).arg(zi.desc).arg(zi.lo);
            }
            strzones += QString("\n");
        }
#else
        if(ranges.size() <= 1) {

            strzones += QString("FROM BEGIN UNTIL END, CP=%1:").arg(cp);

        } else if (i == 0) {

            strzones += QString("FROM BEGIN UNTIL %1, CP=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp);

        } else if (i == ranges.size() - 1) {

            strzones += QString("FROM %1 UNTIL END, CP=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp);
        } else {
            strzones += QString("FROM %1 UNTIL %2, CP=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp);
        }

        strzones += QString("\n");
        for (int j = 0; j < ranges[i].zones.size(); j++) {

            const ZoneInfo &zi = ranges[i].zones[j];

            if (ranges[i].zones[j].hi == INT_MAX) {
                strzones += QString("%1,%2,%3,MAX\n").arg(zi.name).arg(zi.desc).arg(zi.lo);

            } else {

                strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.hi);
            }
        }
        strzones += QString("\n");
#endif
    }

    QFile file(home.canonicalPath() + "/power.zones");
    if (file.open(QFile::WriteOnly)) {

        QTextStream stream(&file);
        stream << strzones;
        file.close();
    } else {
        QMessageBox msgBox;
        msgBox.setIcon(QMessageBox::Critical);
        msgBox.setText(tr("Problem Saving Power Zones"));
        msgBox.setInformativeText(tr("File: %1 cannot be opened for 'Writing'. Please check file properties.").arg(home.canonicalPath() + "/power.zones"));
        msgBox.exec();
        return;
    }
}
Пример #11
0
/* 
 * Copyright (c) 2006 Sean C. Rhea ([email protected])
 *
 * 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 <QMessageBox>
#include "Zones.h"
#include "TimeUtils.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <math.h>

static QList <int> zone_default;
static QList <bool> zone_default_is_pct;
static QList <QString> zone_default_name;
static QList <QString> zone_default_desc;
static int nzones_default = 0;

// the infinity endpoints are indicated with empty dates
static const QDate date_zero     = QDate::QDate();
static const QDate date_infinity = QDate::QDate();

// functions used for sorting zones and ranges
bool Zones::zone_default_index_lessthan(int i1, int i2) {
    return (
	    zone_default[i1] * (zone_default_is_pct[i1] ? 250 : 1) <
	    zone_default[i2] * (zone_default_is_pct[i2] ? 250 : 1)
	    );
}

bool Zones::zoneptr_lessthan(ZoneInfo *z1, ZoneInfo *z2) {
    return (
	    (z1->lo < z2->lo) ||
	    ((z1->lo == z2->lo) && (z1->hi < z2->hi))
	    );
}

bool Zones::rangeptr_lessthan(ZoneRange *r1, ZoneRange *r2) {
    return (
	    (
	     (! r2->begin.isNull()) &&
	     ( r1->begin.isNull() || r1->begin < r2->begin )
	     )
	    ||
	    ((r1->begin == r2->begin) &&
	     (! r1->end.isNull()) &&
	     ( r2->end.isNull() || r1->end < r2->end )
	     )
	    );
}

// initialize default static zone parameters
void Zones::initializeZoneParameters()
{
    static int initial_zone_default[] =
	{
	    0,
	    55,
	    75,
	    90,
	    105,
	    120,
	    150
	};
    static const char *initial_zone_default_desc[] =
	{
	    "Active Recovery",
	    "Endurance",
	    "Tempo",
	    "Threshold",
	    "VO2Max",
	    "Anaerobic",
	    "Neuromuscular"
	};
    static const char *initial_zone_default_name[] =
	{
	    "Z1",
	    "Z2",
	    "Z3",
	    "Z4",
	    "Z5",
	    "Z6",
	    "Z7"
	};

    static int initial_nzones_default =
	sizeof(initial_zone_default) /
	sizeof(initial_zone_default[0]);

    zone_default.clear();
    zone_default_is_pct.clear();
    zone_default_desc.clear();
    zone_default_name.clear();
    nzones_default = 0;

    nzones_default = initial_nzones_default;

    for (int z = 0; z < nzones_default; z ++) {
	zone_default.append(initial_zone_default[z]);
	zone_default_is_pct.append(true);
	zone_default_name.append(QString(initial_zone_default_name[z]));
	zone_default_desc.append(QString(initial_zone_default_desc[z]));
    }

    fprintf(
	    stderr,
	    "%d default zones set:\n",
	    nzones_default
	    );
}


// read zone file, allowing for zones with or without end dates
bool Zones::read(QFile &file) 
{
    defaults_from_user = false;
    zone_default.clear();
    zone_default_is_pct.clear();
    zone_default_name.clear();
    zone_default_desc.clear();
    nzones_default = 0;

    // set up possible warning dialog
    warning = QString();
    int warning_lines = 0;
    const int max_warning_lines = 100;

    // macro to append lines to the warning
    #define append_to_warning(s) \
        if (warning_lines < max_warning_lines) \
	    warning.append(s);  \
        else if (warning_lines == max_warning_lines) \
	    warning.append("...\n"); \
        warning_lines ++;

    // read using text mode takes care of end-lines
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 
        err = "can't open file";
        return false;
    }
    QTextStream fileStream(&file);
    
    QRegExp commentrx("\\s*#.*$");
    QRegExp blankrx("^[ \t]*$");
    QRegExp rangerx[] =
	{
	    QRegExp(
		    "^\\s*(?:from\\s+)?"                                 // optional "from"
		    "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
		    "\\s*([,:]?\\s*(FTP|CP)\\s*=\\s*(\\d+))?"            // optional {CP/FTP = integer (optional %)}
		    "\\s*:?\\s*$",                                       // optional :
		    Qt::CaseInsensitive
		    ),
	    QRegExp(
		    "^\\s*(?:from\\s+)?"                                 // optional "from"
		    "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
		    "\\s+(?:until|to|-)\\s+"                             // until
		    "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?"  // end date
		    "\\s*:?,?\\s*((FTP|CP)\\s*=\\s*(\\d+))?"          // optional {CP/FTP = integer (optional %)}
		    "\\s*:?\\s*$",                                       // optional :
		    Qt::CaseInsensitive
		    )
	};
    QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
		   "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$",
		   Qt::CaseInsensitive
		   );
    QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
			  Qt::CaseInsensitive
			  );

    int lineno = 0;

    // the current range in the file
    ZoneRange *range = NULL;

    // true if zone defaults are found in the file (then we need to write them)
    bool zones_are_defaults = false;

    while (! fileStream.atEnd() ) {
        ++lineno;
	QString line = fileStream.readLine();
        fprintf(stderr, "line %d: \"%s\"\n", 
                lineno, line.toAscii().constData());
        int pos = commentrx.indexIn(line, 0);
        if (pos != -1) {
            fprintf(stderr, "line %d: blank\n", lineno);
            line = line.left(pos);
        }
        if (blankrx.indexIn(line, 0) == 0) {
	    goto next_line;
        }

	// check for default zone range definition (may be followed by zone definitions)
	if (zonedefaultsx.indexIn(line, 0) != -1) {
	    fprintf(stderr, "line %d: zone defaults specification identified\n", lineno);

	    zones_are_defaults = true;

	    // defaults are allowed only at the beginning of the file
	    if (ranges.size()) {
		err = "Zone defaults must be specified at head of power.zones file";
		return false;
	    }

	    // only one set of defaults is allowed
	    if (nzones_default) {
		err = "Only one set of zone defaults may be specified in power.zones file";
		return false;
	    }
	    
	    goto next_line;
	}

	// check for range specification (may be followed by zone definitions)
	for (int r = 0; r < 2; r++) {
	    if (rangerx[r].indexIn(line, 0) != -1) {
		zones_are_defaults = false;

		fprintf(stderr, "line %d: matched range: %s to %s\n", 
			lineno,
			rangerx[r].cap(1).toAscii().constData(),
			rangerx[r].cap(5).toAscii().constData());
		QDate begin, end;

		// process the beginning date
		if (rangerx[r].cap(1) == "BEGIN")
		    begin = date_zero;
		else {
		    begin = QDate(rangerx[r].cap(2).toInt(),
				  rangerx[r].cap(3).toInt(),
				  rangerx[r].cap(4).toInt()
				  );
		}

		// process an end date, if any, else it is null
		if (rangerx[r].cap(5) == "END")
		    end = date_infinity;
		else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) {
		    end = QDate(rangerx[r].cap(6).toInt(),
				rangerx[r].cap(7).toInt(),
				rangerx[r].cap(8).toInt()
				);
		    fprintf(stderr, "end date = %s\n", end.toString().toAscii().constData());
		}
		else {
		    end = QDate();
		    fprintf(stderr, "no end date\n");
		}

		if (range) {
		    // if zones are empty, then generate them
		    if (range->zones.empty()) {
			fprintf(stderr, "create zones from cp = %d\n", range->cp);
			if (range->cp > 0)
			    setZonesFromCP(range);
		
			else {
			    err = tr("line %1: read new range without reading "
				     "any zones for previous one").arg(lineno);
			    file.close();
			    return false;
			}
		    }
		    // else sort them
		    else {
			fprintf(stderr, "Sorting zones for range %d\n", ranges.size() + 1);
			qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan);
		    }

		    ranges.append(range);
		}

		// set up the range, capturing CP if it's specified
		range = new ZoneRange(begin, end);
		int nCP = (r ? 11 : 7);
		if (rangerx[r].numCaptures() == (nCP)) {
		    range->cp = rangerx[r].cap(nCP).toInt();
		    fprintf(stderr, "setting CP = %d\n", range->cp);
		}
		goto next_line;
	    }
	}

	// check for zone definition
        if (zonerx.indexIn(line, 0) != -1) {
	    if (! (range || zones_are_defaults)) {
		err = tr("line %1: read zone without "
			 "preceeding date range").arg(lineno);
		file.close();
		return false;
	    }

	    int lo = zonerx.cap(3).toInt();

	    // allow for zone specified as % of CP
	    bool lo_is_pct = false;
	    if (zonerx.cap(4) == "%")
		if (zones_are_defaults)
		    lo_is_pct = true;
		else if (range->cp > 0)
		    lo = int(lo * range->cp / 100);
		else {
		    err = tr("attempt to set zone based on % of "
			     "CP without setting CP in line number %1.\n").
			arg(lineno);
		    file.close();
		    return false;
		}

	    int hi;
	    // if this is not a zone defaults specification, process possible hi end of zones
	    if (zones_are_defaults || zonerx.cap(5).isEmpty())
		hi = -1;   // signal an undefined number
	    else if (zonerx.cap(5) == "MAX")
		hi = INT_MAX;
	    else {
		hi = zonerx.cap(5).toInt();

		// allow for zone specified as % of CP
		if (zonerx.cap(5) == "%")
		    if (range->cp > 0)
			hi = int(hi * range->cp / 100);
		    else {
			err = tr("attempt to set zone based on % of CP "
				 "without setting CP in line number %1.\n").
			    arg(lineno);
			file.close();
			return false;
		    }
	    }

	    if (zones_are_defaults) {
		nzones_default ++;
		zone_default_is_pct.append(lo_is_pct);
		zone_default.append(lo);
		zone_default_name.append(zonerx.cap(1));
		zone_default_desc.append(zonerx.cap(2));
		fprintf(stderr,
			"line %d: zone default #%d found: \"%s\" (%s): %d%s\n",
			lineno,
			nzones_default,
			zone_default_name[nzones_default - 1].toAscii().constData(),
			zone_default_desc[nzones_default - 1].toAscii().constData(),
			zone_default[nzones_default - 1],
			zone_default_is_pct[nzones_default - 1] ? "%" : ""
			);
		defaults_from_user = true;
	    }
	    else {
		ZoneInfo *zone = 
		    new ZoneInfo(zonerx.cap(1), zonerx.cap(2), lo, hi);
		fprintf(stderr, "line %d: matched zones: "
			"\"%s\", \"%s\", %s, %s\n",
			lineno,
			zonerx.cap(1).toAscii().constData(),
			zonerx.cap(2).toAscii().constData(),
			zonerx.cap(3).isEmpty() ? "null" : zonerx.cap(3).toAscii().constData(),
			zonerx.cap(4).isEmpty() ? "null" : zonerx.cap(4).toAscii().constData()
			);

		range->zones.append(zone);
	    }
	}
    next_line: {}
    }

    if (range) {
        if (range->zones.empty()) {
	    fprintf(stderr, "empty zones found: cp = %d\n", range->cp);
  	    if (range->cp > 0)
	        setZonesFromCP(range);
	    else {
                err = tr("file ended without reading any zones for last range");
                file.close();
                return false;
	    }
        }
	else {
	    fprintf(stderr, "Sorting zones for final range %d\n", ranges.size());
	    qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan);

	}

        ranges.append(range);
    }
    file.close();

    // sort the ranges
    qSort(ranges.begin(), ranges.end(), rangeptr_lessthan);

    // sort the zone defaults, as best we can (may not be right if there's
    // a mix of % and absolute ranges)
    if (nzones_default)  {
	fprintf(stderr, "Sorting zone defaults...\n");
	QVector <int> zone_default_index(nzones_default);
	for (int i = 0; i < nzones_default; i++)
	    zone_default_index[i] = i;
	qSort(zone_default_index.begin(), zone_default_index.end(), zone_default_index_lessthan);

	QVector <int> zone_default_new(nzones_default);
	QVector <bool> zone_default_is_pct_new(nzones_default);
	QVector <QString> zone_default_name_new(nzones_default);
	QVector <QString> zone_default_desc_new(nzones_default);

	for (int i = 0; i < nzones_default; i++) {
	    zone_default_new[i]        = zone_default[zone_default_index[i]];
	    zone_default_is_pct_new[i] = zone_default_is_pct[zone_default_index[i]];
	    zone_default_name_new[i]   = zone_default_name[zone_default_index[i]];
	    zone_default_desc_new[i]   = zone_default_desc[zone_default_index[i]];
	}

	for (int i = 0; i < nzones_default; i++) {
	    zone_default[i]            = zone_default_new[i];
	    zone_default_is_pct[i]     = zone_default_is_pct_new[i];
	    zone_default_name[i]       = zone_default_name_new[i];
	    zone_default_desc[i]       = zone_default_desc_new[i];
	}
    }

    // resolve undefined endpoints in ranges and zones
    for (int nr = 0; nr < ranges.size(); nr ++) {
	// clean up gaps or overlaps in zone ranges
	if (ranges[nr]->end.isNull())
	    ranges[nr]->end =
		(nr < ranges.size() - 1) ?
		ranges[nr + 1]->begin :
		date_infinity;
	else if ((nr < ranges.size() - 1) &&
		 (ranges[nr + 1]->begin != ranges[nr]->end)
		 ) {

	    append_to_warning(tr("Setting end date of range %1 "
				 "to start date of range %2.\n").
			      arg(nr + 1).
			      arg(nr + 2)
			      );

	    ranges[nr]->end = ranges[nr + 1]->begin;
	}
	else if ((nr == ranges.size() - 1) &&
		 (ranges[nr]->end < QDate::currentDate())
		 ) {

	    append_to_warning(tr("Extending final range %1 to infinite "
				 "to include present date.\n").arg(nr + 1));
	    
	    ranges[nr]->end = date_infinity;
	}

	if (ranges[nr]->zones.size()) {
	    // check that the first zone starts with zero
	    ranges[nr]->zones[0]->lo = 0;
    
	    // resolve zone end powers
	    for (int nz = 0; nz < ranges[nr]->zones.size(); nz ++)
		if (ranges[nr]->zones[nz]->hi == -1)
		    ranges[nr]->zones[nz]->hi =
			(nz < ranges[nr]->zones.size() - 1) ?
			ranges[nr]->zones[nz + 1]->lo :
			INT_MAX;
		else if ((nz < ranges[nr]->zones.size() - 1) &&
			 (ranges[nr]->zones[nz]->hi != ranges[nr]->zones[nz + 1]->lo)
			 ) {
		    if (abs(ranges[nr]->zones[nz]->hi - ranges[nr]->zones[nz + 1]->lo) > 4)
			append_to_warning(tr("Range %1: matching top of zone %2 "
					     "(%3) to bottom of zone %4 (%5).\n").
					  arg(nr + 1).
					  arg(ranges[nr]->zones[nz]->name).
					  arg(ranges[nr]->zones[nz]->hi).
					  arg(ranges[nr]->zones[nz + 1]->name).
					  arg(ranges[nr]->zones[nz + 1]->lo)
					  );
		    ranges[nr]->zones[nz]->hi = ranges[nr]->zones[nz + 1]->lo;
		}
		else if ((nz == ranges[nr]->zones.size() - 1) &&
			 (ranges[nr]->zones[nz]->hi < INT_MAX)
			 ) {
		    append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n").
				      arg(nr + 1).
				      arg(ranges[nr]->zones[nz]->name).
				      arg(ranges[nr]->zones[nz]->hi)
				      );
		    ranges[nr]->zones[nz]->hi = INT_MAX;
		}
	}

	fprintf(stderr, "sorted range %d: from %s to %s with %s zones\n",
		nr + 1,
		ranges[nr]->begin.isNull() ? "BEGIN" : ranges[nr]->begin.toString().toAscii().constData(),
		ranges[nr]->end.isNull() ? "END" : ranges[nr]->end.toString().toAscii().constData(),
		ranges[nr]->zonesSetFromCP ? "calculated" : "specified"
		);
    }

    // mark zones as modified so pages which depend on zones can be updated
    modificationTime = QDateTime::currentDateTime();

    return true;
}

// note empty dates are treated as automatic matches for begin or
// end of range
int Zones::whichRange(const QDate &date) const
{
    int rnum = 0;
    QListIterator<ZoneRange*> i(ranges);
    while (i.hasNext()) {
        ZoneRange *range = i.next();
        if (((date >= range->begin) || (range->begin.isNull())) &&
	    ((date < range->end) || (range->end.isNull()))
	    )
            return rnum;
        ++rnum;
    }
    return -1;
}

int Zones::numZones(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum]->zones.size();
}

int Zones::whichZone(int rnum, double value) const 
{
    assert(rnum < ranges.size());
    ZoneRange *range = ranges[rnum];
    for (int j = 0; j < range->zones.size(); ++j) {
        ZoneInfo *info = range->zones[j];
	// note: the "end" of range is actually in the next zone
        if ((value >= info->lo) && (value < info->hi))
            return j;
    }
    return -1;
}

void Zones::zoneInfo(int rnum, int znum, 
                     QString &name, QString &description,
                     int &low, int &high) const
{
    assert(rnum < ranges.size());
    ZoneRange *range = ranges[rnum];
    assert(znum < range->zones.size());
    ZoneInfo *zone = range->zones[znum];
    name = zone->name;
    description = zone->desc;
    low = zone->lo;
    high = zone->hi;
}

int Zones::getCP(int rnum) const
{
    assert(rnum < ranges.size());
    return ranges[rnum]->cp;
}

void Zones::setCP(int rnum, int cp)
{
    ranges[rnum]->cp = cp;
    modificationTime = QDateTime::currentDateTime();
}

// generate a list of zones from CP
int Zones::lowsFromCP(QList <int> *lows, int cp) {
    if (nzones_default == 0)
	initializeZoneParameters();

    lows->clear();
   
    for (int z = 0; z < nzones_default; z++)
	lows->append(zone_default_is_pct[z] ? zone_default[z] * cp / 100 : zone_default[z]);

    return nzones_default;
}

// access the zone name
QString Zones::getDefaultZoneName(int z) {
    return zone_default_name[z];
}

// access the zone description
QString Zones::getDefaultZoneDesc(int z) {
    return zone_default_desc[z];
}

// set the zones from the CP value (the cp variable)
void Zones::setZonesFromCP(ZoneRange *range) {
    range->zones.clear();

    if (nzones_default == 0)
	initializeZoneParameters();

    for (int i = 0; i < nzones_default; i++) {
	int lo = zone_default_is_pct[i] ? zone_default[i] * range->cp / 100 : zone_default[i];
	int hi = lo;
	ZoneInfo *zone = 
	    new ZoneInfo(zone_default_name[i], zone_default_desc[i], lo, hi);
	range->zones.append(zone);
    }

    // sort the zones (some may be pct, others absolute, so zones need to be sorted,
    // rather than the defaults
    qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan);

    // set zone end dates
    for (int i = 0; i < range->zones.size(); i ++)
	range->zones[i]->hi =
	    (i < nzones_default - 1) ?
	    range->zones[i + 1]->lo :
	    INT_MAX;

    // mark that the zones were set from CP, so if zones are subsequently
    // written, only CP is saved
    range->zonesSetFromCP = true;
}

void Zones::setZonesFromCP(int rnum) {
    assert((rnum >= 0) && (rnum < ranges.size()));
    setZonesFromCP(ranges[rnum]);
}

// return the list of starting values of zones for a given range
QList <int> Zones::getZoneLows(int rnum) {
  if (rnum >= ranges.size())
    return QList <int>::QList();
  ZoneRange *range = ranges[rnum];
  QList <int> return_values;
  for (int i = 0; i < range->zones.size(); i ++)
      return_values.append(ranges[rnum]->zones[i]->lo);
  return return_values;
}

// return the list of ending values of zones for a given range
QList <int> Zones::getZoneHighs(int rnum) {
  if (rnum >= ranges.size())
    return QList <int>::QList();
  ZoneRange *range = ranges[rnum];
  QList <int> return_values;
  for (int i = 0; i < range->zones.size(); i ++)
      return_values.append(ranges[rnum]->zones[i]->hi);
  return return_values;
}


// return the list of zone names
QList <QString> Zones::getZoneNames(int rnum) {
  if (rnum >= ranges.size())
    return QList <QString>::QList();
  ZoneRange *range = ranges[rnum];
  QList <QString> return_values;
  for (int i = 0; i < range->zones.size(); i ++)
      return_values.append(ranges[rnum]->zones[i]->name);
  return return_values;
  }

QString Zones::summarize(int rnum, QVector<double> &time_in_zone) const
{
    assert(rnum < ranges.size());
    ZoneRange *range = ranges[rnum];
    assert(time_in_zone.size() == range->zones.size());
    QString summary;
    if(range->cp > 0){
        summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
        summary += "<tr><td align=\"center\">";
        summary += tr("Critical Power: %1").arg(range->cp);
        summary += "</td></tr></table>";
    }
    summary += "<table align=\"center\" width=\"70%\" ";
    summary += "border=\"0\">";
    summary += "<tr>";
    summary += "<td align=\"center\">Zone</td>";
    summary += "<td align=\"center\">Description</td>";
    summary += "<td align=\"center\">Low</td>";
    summary += "<td align=\"center\">High</td>";
    summary += "<td align=\"center\">Time</td>";
    summary += "</tr>";
    QColor color = QApplication::palette().alternateBase().color();
    color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value());
    for (int zone = 0; zone < time_in_zone.size(); ++zone) {
        if (time_in_zone[zone] > 0.0) {
            QString name, desc;
            int lo, hi;
            zoneInfo(rnum, zone, name, desc, lo, hi);
            if (zone % 2 == 0)
                summary += "<tr bgcolor='" + color.name() + "'>";
            else
                summary += "<tr>";
            summary += QString("<td align=\"center\">%1</td>").arg(name);
            summary += QString("<td align=\"center\">%1</td>").arg(desc);
            summary += QString("<td align=\"center\">%1</td>").arg(lo);
            if (hi == INT_MAX)
                summary += "<td align=\"center\">MAX</td>";
            else
                summary += QString("<td align=\"center\">%1</td>").arg(hi);
            summary += QString("<td align=\"center\">%1</td>")
                .arg(time_to_string((unsigned) round(time_in_zone[zone])));
            summary += "</tr>";
        }
    }
    summary += "</table>";
    return summary;
}

#define USE_SHORT_POWER_ZONES_FORMAT true   /* whether a less redundent format should be used */
void Zones::write(QDir home)
{
    QString strzones;

    // write the defaults if they were specified by the user
    if (defaults_from_user) {
	strzones += QString("DEFAULTS:\n");
	for (int z = 0 ; z < nzones_default; z ++)
	    strzones += QString("%1,%2,%3%4\n").
		arg(zone_default_name[z]).
		arg(zone_default_desc[z]).
		arg(zone_default[z]).
		arg(zone_default_is_pct[z]?"%":"");
	strzones += QString("\n");
    }
    for (int i = 0; i < ranges.size(); i++)
    {
        int cp = getCP(i);

	// print header for range
	// note this explicitly sets the first and last ranges such that all time is spanned

        #if USE_SHORT_POWER_ZONES_FORMAT
        if (i == 0)
            strzones += QString("BEGIN: CP=%1").arg(cp);
	else
            strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp);
	strzones += QString("\n");

	// step through and print the zones if they've been explicitly set
	if (! ranges[i]->zonesSetFromCP) {
	    for (int j = 0; j < ranges[i]->zones.size(); j ++)
		strzones += QString("%1,%2,%3\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo);
	    strzones += QString("\n");
	}
        #else
        if(ranges.size() <= 1)
            strzones += QString("FROM BEGIN UNTIL END, CP=%1:").arg(cp);
        else if (i == 0)
            strzones += QString("FROM BEGIN UNTIL %1, CP=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp);
        else if (i == ranges.size() - 1)
            strzones += QString("FROM %1 UNTIL END, CP=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp);
        else
            strzones += QString("FROM %1 UNTIL %2, CP=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp);
	strzones += QString("\n");
	for (int j = 0; j < ranges[i]->zones.size(); j ++)
	    if (ranges[i]->zones[j]->hi == INT_MAX)
		strzones += QString("%1,%2,%3,MAX\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo);
	    else
		strzones += QString("%1,%2,%3,%4\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo).arg(ranges[i]->zones[j]->hi);
	strzones += QString("\n");
        #endif
    }


    QFile file(home.absolutePath() + "/power.zones");
    if (file.open(QFile::WriteOnly))
    {
        QTextStream stream(&file);
        stream << strzones;
        file.close();
    }
}