Files
GoldenCheetah/src/Core/RideDB.y
Alejandro Martinez 3395c7a4c9 Fix 3b50dd8 for reentrancy
Changed static variable for a member one,
the problem was apparent when opening a
different athlete besides the initial one.
2024-04-12 12:10:50 -03:00

1081 lines
50 KiB
Plaintext

%{
/*
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "RideDB.h"
#include "RideFileCache.h"
#include "Settings.h"
#ifdef GC_WANT_HTTP
#include "APIWebService.h"
#endif
#define YYSTYPE QString
// Lex scanner
extern int RideDBlex(YYSTYPE*,void*); // the lexer aka yylex()
extern int RideDBlex_init(void**);
extern void RideDB_setString(QString, void *);
extern int RideDBlex_destroy(void*); // the cleaner for lexer
// yacc parser
void RideDBerror(void*jc, const char *error) // used by parser aka yyerror()
{ static_cast<RideDBContext*>(jc)->errors << error; }
// extract scanner from the context
#define scanner jc->scanner
%}
%pure-parser
%lex-param { void *scanner }
%parse-param { struct RideDBContext *jc }
%token STRING
%token VERSION RIDES METRICS TAGS XDATA INTERVALS
%start document
%%
/* We allow a .json file to be encapsulated within optional braces */
document: '{' elements '}'
elements: element
| elements ',' element
;
element: version
| RIDES ':' '[' ride_list ']';
version: VERSION ':' string {
if ($3 != RIDEDB_VERSION) {
jc->old=true;
jc->item.isstale=true; // force refresh after load
}
}
ride_list: ride
| ride_list ',' ride
;
ride: '{' rideelement_list '}' {
// search for one to update using serial search,
// if the performance is too slow we can move to
// a binary search, but suspect this ok < 10000 rides
if (jc->api != NULL) {
#ifdef GC_WANT_HTTP
// we're listing rides in the api
jc->api->writeRideLine(jc->item, jc->request, jc->response);
#endif
} else {
double progress= round(double(jc->loading++) / double(jc->cache->rides().count()) * 100.0f);
if (progress > jc->lastProgressUpdate) {
jc->context->notifyLoadProgress(jc->folder,progress);
jc->lastProgressUpdate = progress;
}
// find entry and update it
int index=jc->cache->find(&jc->item);
if (index==-1) qDebug()<<"unable to load:"<<jc->item.fileName<<jc->item.dateTime<<jc->item.weight;
else jc->cache->rides().at(index)->setFrom(jc->item);
}
// now set our ride item clean again, so we don't
// overwrite with prior data
jc->item.metadata().clear();
jc->item.xdata().clear();
jc->item.metrics().fill(0.0f);
jc->item.counts().fill(0.0f);
jc->item.stdmeans().clear();
jc->item.stdvariances().clear();
jc->interval.metrics().fill(0.0f);
jc->interval.counts().fill(0.0f);
jc->interval.stdmeans().clear();
jc->interval.stdvariances().clear();
jc->interval.route = QUuid();
jc->item.clearIntervals();
jc->item.overrides_.clear();
jc->item.fileName = "";
jc->count = "";
jc->value = "";
}
rideelement_list: rideelement_list ',' rideelement
| rideelement ;
rideelement: ride_tuple
| metrics
| tags
| xdata
| intervals
;
/*
* RIDES
*/
/* RideItem state data */
ride_tuple: string ':' string {
if ($1 == "filename") jc->item.fileName = $3;
else if ($1 == "fingerprint") jc->item.fingerprint = $3.toULongLong();
else if ($1 == "crc") jc->item.crc = $3.toULongLong();
else if ($1 == "metacrc") jc->item.metacrc = $3.toULongLong();
else if ($1 == "timestamp") jc->item.timestamp = $3.toULongLong();
else if ($1 == "dbversion") jc->item.dbversion = $3.toInt();
else if ($1 == "udbversion") jc->item.udbversion = $3.toInt();
else if ($1 == "color") jc->item.color = QColor($3);
else if ($1 == "aero") jc->item.isAero = $3.toInt() ? true: false;
else if ($1 == "sport") {
jc->item.sport = ($3);
jc->item.isBike=jc->item.isRun=jc->item.isSwim=jc->item.isXtrain=jc->item.isAero=false;
if ($3 == "Bike") jc->item.isBike = true;
else if ($3 == "Run") jc->item.isRun = true;
else if ($3 == "Swim") jc->item.isSwim = true;
else if ($3 == "Aero") jc->item.isAero = true;
else jc->item.isXtrain = true;
}
else if ($1 == "present") jc->item.present = $3;
else if ($1 == "overrides") jc->item.overrides_ = $3.split(",");
else if ($1 == "weight") jc->item.weight = $3.toDouble();
else if ($1 == "samples") jc->item.samples = $3.toInt() > 0 ? true : false;
else if ($1 == "zonerange") jc->item.zoneRange = $3.toInt();
else if ($1 == "hrzonerange") jc->item.hrZoneRange = $3.toInt();
else if ($1 == "pacezonerange") jc->item.paceZoneRange = $3.toInt();
else if ($1 == "date") {
QDateTime aslocal = QDateTime::fromString($3, DATETIME_FORMAT);
QDateTime asUTC = QDateTime(aslocal.date(), aslocal.time(), Qt::UTC);
jc->item.dateTime = asUTC.toLocalTime();
}
}
/*
* RIDE METRIC values
*/
metrics: METRICS ':' '{' metrics_list '}' ;
metrics_list: metric | metrics_list ',' metric ;
metric: metric_key ':' metric_value /* metric computed value */
{
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->item.metrics()[m->index()] = $3.toDouble();
jc->item.counts()[m->index()] = 0; // we don't write zero values
}
else qDebug()<<"metric not found:"<<$1;
}
| metric_key ':' '[' metric_value ',' metric_count ']' {
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->item.metrics()[m->index()] = $4.toDouble();
jc->item.counts()[m->index()] = $6.toDouble();
}
else qDebug()<<"metric not found:"<<$1;
}
| metric_key ':' '[' metric_value ',' metric_count ',' metric_stdmean ',' metric_stdvariance ']' {
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->item.metrics()[m->index()] = $4.toDouble();
jc->item.counts()[m->index()] = $6.toDouble();
jc->item.stdmeans().insert(m->index(), $8.toDouble());
jc->item.stdvariances().insert(m->index(), $10.toDouble());
}
else qDebug()<<"metric not found:"<<$1;
}
metric_key : string { jc->key = jc->JsonString; }
metric_value : string { jc->value = jc->JsonString; }
metric_count : string { jc->count = jc->JsonString; }
metric_stdmean : string { jc->count = jc->JsonString; }
metric_stdvariance : string { jc->count = jc->JsonString; }
/*
* INTERVALS
*/
intervals: INTERVALS ':' '[' intervals_list ']' ;
intervals_list: intervals_list ',' interval
| interval
;
interval: '{' intervalelement_list '}' {
jc->item.addInterval(jc->interval);
jc->interval.metrics().fill(0.0f);
jc->interval.counts().fill(0.0f);
jc->interval.stdmeans().clear();
jc->interval.stdvariances().clear();
}
intervalelement_list: intervalelement_list ',' interval_element
| interval_element
;
interval_element: interval_tuple
| interval_metrics
;
/* IntervalItem state data */
interval_tuple: string ':' string {
if ($1 == "name") jc->interval.name = $3;
else if ($1 == "start") jc->interval.start = $3.toDouble();
else if ($1 == "stop") jc->interval.stop = $3.toDouble();
else if ($1 == "startKM") jc->interval.startKM = $3.toDouble();
else if ($1 == "stopKM") jc->interval.stopKM = $3.toDouble();
else if ($1 == "type") jc->interval.type = static_cast<RideFileInterval::intervaltype>($3.toInt());
else if ($1 == "color") jc->interval.color = QColor($3);
else if ($1 == "seq") jc->interval.displaySequence = $3.toInt();
else if ($1 == "route") jc->interval.route = QUuid($3);
else if ($1 == "test") jc->interval.test = $3 == "true" ? true : false;
}
interval_metrics: METRICS ':' '{' interval_metrics_list '}' ;
interval_metrics_list: interval_metric | interval_metrics_list ',' interval_metric ;
interval_metric: interval_metric_key ':' interval_metric_value /* metric computed value */
{
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->interval.metrics()[m->index()] = $3.toDouble();
jc->interval.counts()[m->index()] = 0; /* we don't write zeroes */
} else qDebug()<<"metric not found:"<<$1;
}
| interval_metric_key ':' '[' interval_metric_value ',' interval_metric_count ']'
{
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->interval.metrics()[m->index()] = $4.toDouble();
jc->interval.counts()[m->index()] = $6.toDouble();
} else qDebug()<<"metric not found:"<<$1;
}
| interval_metric_key ':' '[' interval_metric_value ',' interval_metric_count ',' interval_metric_stdmean ',' interval_metric_stdvariance ']'
{
const RideMetric *m = RideMetricFactory::instance().rideMetric($1);
if (m) {
jc->interval.metrics()[m->index()] = $4.toDouble();
jc->interval.counts()[m->index()] = $6.toDouble();
jc->interval.stdmeans().insert(m->index(), $8.toDouble());
jc->interval.stdvariances().insert(m->index(), $10.toDouble());
} else qDebug()<<"metric not found:"<<$1;
}
;
interval_metric_key : string { jc->key = jc->JsonString; }
interval_metric_value : string { jc->value = jc->JsonString; }
interval_metric_count : string { jc->count = jc->JsonString; }
interval_metric_stdmean : string { jc->count = jc->JsonString; }
interval_metric_stdvariance : string { jc->count = jc->JsonString; }
/*
* Metadata TAGS
*/
tags: TAGS ':' '{' tags_list '}' ;
tags_list: tag | tags_list ',' tag ;
tag: tag_key ':' tag_value /* metadata value */
{
jc->item.metadata().insert(jc->key, jc->value);
}
tag_key : string { jc->key = jc->JsonString; }
tag_value : string { jc->value = jc->JsonString; }
/*
* XDATA definitions
*/
xdata: XDATA ':' '{' xdata_list '}'
xdata_list: xdata | xdata_list ',' xdata ;
xdata: xdata_name ':' '[' xdata_values ']' { jc->item.xdata().insert(jc->key,jc->JsonStringList);
jc->key = ""; jc->JsonStringList.clear(); }
xdata_name : string { jc->key = jc->JsonString; }
xdata_values:
| xdata_value
| xdata_value ',' xdata_values
;
xdata_value: string { jc->JsonStringList << jc->JsonString; }
/*
* Primitives
*/
string: STRING { jc->JsonString = $1; }
;
%%
void
RideCache::load()
{
// only load if it exists !
QFile rideDB(QString("%1/%2").arg(context->athlete->home->cache().canonicalPath()).arg("rideDB.json"));
if (rideDB.exists() && rideDB.open(QFile::ReadOnly)) {
QDir directory = context->athlete->home->activities();
QDir plannedDirectory = context->athlete->home->planned();
// ok, lets read it in
// Read the entire file into a QString -- we avoid using fopen since it
// doesn't handle foreign characters well. Instead we use QFile and parse
// from a QString
QString contents = QString(rideDB.readAll());
rideDB.close();
// create scanner context for reentrant parsing
RideDBContext *jc = new RideDBContext;
jc->context = context;
jc->cache = this;
jc->api = NULL;
jc->old = false;
jc->loading = 0;
jc->lastProgressUpdate = 0.0;
jc->folder = context->athlete->home->root().canonicalPath();
// clean item
jc->item.path = directory.canonicalPath(); // TODO use plannedDirectory for planned
jc->item.context = context;
jc->item.isstale = jc->item.isdirty = jc->item.isedit = false;
RideDBlex_init(&scanner);
// inform the parser/lexer we have a new file
RideDB_setString(contents, scanner);
// setup
jc->errors.clear();
// set to non-zero if you want to
// to debug the yyparse() state machine
// sending state transitions to stderr
//yydebug = 0;
// parse it
RideDBparse(jc);
// clean up
RideDBlex_destroy(scanner);
// regardless of errors we're done !
delete jc;
return;
}
}
// Escape special characters (JSON compliance)
static QString protect(const QString string)
{
QString s = string;
s.replace("\\", "\\\\"); // backslash
s.replace("\"", "\\\""); // quote
s.replace("\t", "\\t"); // tab
s.replace("\n", "\\n"); // newline
s.replace("\r", "\\r"); // carriage-return
s.replace("\b", "\\b"); // backspace
s.replace("\f", "\\f"); // formfeed
s.replace("/", "\\/"); // solidus
// add a trailing space to avoid conflicting with GC special tokens
s += " ";
return s;
}
static QVector<int> mmp_durations;
static bool setup_mmp_durations()
{
// 1s - 3mins every second
for(int i=1; i<=180; i++) mmp_durations << i;
// 3mins to 10mins every 5 seconds
for(int i=185; i<=600; i+=5) mmp_durations << i;
// 10mins to 30 mins every 30 seconds
for(int i=630; i<=1800; i+=30) mmp_durations << i;
// 30mins to 2hrs every minute
for(int i=1860; i<=(120*60); i += 60) mmp_durations << i;
// 2hrs+ every 30 mins
for(int i=(120*60)+1800; i <= (10 * 60 * 60); i+=1800) mmp_durations << i;
return true;
}
static bool did_mmp_durations = setup_mmp_durations();
// Fast constructon of common string. In the future we'll achieve this with a single variadic template function.
QString ConstructNameNumberString(QString s0, QString name, QString s1, double num, QString s2)
{
QString numStr;
numStr.setNum(num, 'f', 5);
QString m;
m.reserve(s0.length() + name.length() + s1.length() + numStr.length() + s2.length());
m.append(s0);
m.append(name);
m.append(s1);
m.append(numStr);
m.append(s2);
return m;
}
// Fast constructon of common string. In the future we'll achieve this with a single variadic template function.
QString ConstructNameNumberNumberString(QString s0, QString name, QString s1, double num0, QString s2, double num1, QString s3)
{
QString num0Str;
num0Str.setNum(num0, 'f', 5);
QString num1Str;
num1Str.setNum(num1, 'f', 5);
QString m;
m.reserve(s0.length() + name.length() + s1.length() + num0Str.length() + s2.length() + num1Str.length() + s3.length());
m.append(s0);
m.append(name);
m.append(s1);
m.append(num0Str);
m.append(s2);
m.append(num1Str);
m.append(s3);
return m;
}
// save cache to disk
//
// if opendata is true then save in format for sending to the GC OpenData project
// the filename may be supplied if exporting for other purposes, if empty then save
// to ~athlete/cache/rideDB.json
//
// When writing for opendata the file this doesn't (and must not) contain PII or
// metadata, but does include some distributions for Heartrate, Power, Cadence
// and Speed along with MMP data. We need to consider what athlete info to add
// possibly only gender and year of birth.
//
// it must be valid json and can be parse with python using
// import json
// with open('rideDB.json') as json_data:
// d = json.load(json_data)
// print(len(d["RIDES"])
//
void RideCache::save(bool opendata, QString filename)
{
// now save data away - use passed filename if set
QFile rideDB(QString("%1/%2").arg(context->athlete->home->cache().canonicalPath()).arg("rideDB.json"));
if (filename != "") rideDB.setFileName(filename);
if (rideDB.open(QFile::WriteOnly)) {
const RideMetricFactory &factory = RideMetricFactory::instance();
// ok, lets write out the cache
QTextStream stream(&rideDB);
#if QT_VERSION < 0x060000
stream.setCodec("UTF-8");
#endif
// no BOM needed for opendata as it doesn't contain textual data
if (!opendata) stream.setGenerateByteOrderMark(true);
stream << "{" ;
stream << QString("\n \"VERSION\":\"%1\",").arg(RIDEDB_VERSION);
// send very basic athlete info with no PII
// year of birth, gender, uuid
if (opendata) {
stream << QString("\n \"ATHLETE\":{ \"gender\":\"%1\", \"yob\":\"%2\", \"id\":\"%3\" },")
.arg(appsettings->cvalue(context->athlete->cyclist, GC_SEX).toInt() == 0 ? "M" : "F")
.arg(appsettings->cvalue(context->athlete->cyclist, GC_DOB).toDate().year())
.arg(context->athlete->id.toString());
}
stream << "\n \"RIDES\":[\n";
bool firstRide = true;
foreach(RideItem *item, rides()) {
// skip if not loaded/refreshed, a special case
// if saving during an initial refresh
if (item->metrics().count() == 0) continue;
// don't save files with discarded changes at exit
if (item->skipsave == true) continue;
// comma separate each ride
if (!firstRide) stream << ",\n";
firstRide = false;
// basic ride information
stream << "\t{\n";
stream << "\t\t\"date\":\"" <<item->dateTime.toUTC().toString(DATETIME_FORMAT) << "\",\n";
if (!opendata) {
// we don't send this info when sharing as opendata
stream << "\t\t\"filename\":\"" <<item->fileName <<"\",\n";
stream << "\t\t\"fingerprint\":\"" <<item->fingerprint <<"\",\n";
stream << "\t\t\"crc\":\"" <<item->crc <<"\",\n";
stream << "\t\t\"metacrc\":\"" <<item->metacrc <<"\",\n";
stream << "\t\t\"timestamp\":\"" <<item->timestamp <<"\",\n";
stream << "\t\t\"dbversion\":\"" <<item->dbversion <<"\",\n";
stream << "\t\t\"udbversion\":\"" <<item->udbversion <<"\",\n";
stream << "\t\t\"color\":\"" <<item->color.name() <<"\",\n";
stream << "\t\t\"present\":\"" <<item->present <<"\",\n";
stream << "\t\t\"sport\":\"" <<item->sport <<"\",\n";
stream << "\t\t\"aero\":\"" << (item->isAero ? "1" : "0") <<"\",\n";
stream << "\t\t\"weight\":\"" <<item->weight <<"\",\n";
if (item->zoneRange >= 0) stream << "\t\t\"zonerange\":\"" <<item->zoneRange <<"\",\n";
if (item->hrZoneRange >= 0) stream << "\t\t\"hrzonerange\":\"" <<item->hrZoneRange <<"\",\n";
if (item->paceZoneRange >= 0) stream << "\t\t\"pacezonerange\":\"" <<item->paceZoneRange <<"\",\n";
// if there are overrides, do share them
if (item->overrides_.count()) stream << "\t\t\"overrides\":\"" <<item->overrides_.join(",") <<"\",\n";
stream << "\t\t\"samples\":\"" <<(item->samples ? "1" : "0") <<"\",\n";
} else {
// need to know what data was collected
stream << "\t\t\"data\":\"" <<item->getText("Data","") <<"\",\n";
stream << "\t\t\"sport\":\"" <<item->getText("Sport","") <<"\",\n";
}
// pre-computed metrics
stream << "\n\t\t\"METRICS\":{\n";
bool firstMetric = true;
for(int i=0; i<factory.metricCount(); i++) {
QString name = factory.metricName(i);
int index = factory.rideMetric(name)->index();
// don't output 0, nan or inf values, they're set to 0 by default
// unless aggregateZero indicates the count is relevant
if ((!std::isinf(item->metrics()[index]) && !std::isnan(item->metrics()[index]) &&
(item->metrics()[index] > 0.00f || item->metrics()[index] < 0.00f)) ||
(item->metrics()[index] == 0.00f && item->counts()[index] > 1.0 && factory.rideMetric(name)->aggregateZero())) {
if (!firstMetric) stream << ",\n";
firstMetric = false;
// if stdmean or variance is non-zero we write all 4
if (item->stdmeans().value(index, 0.0f) || item->stdvariances().value(index, 0.0f)) {
stream << "\t\t\t\"" << name << "\":[\"" << QString("%1").arg(item->metrics()[index], 0, 'f', 5) <<"\",\""
<< QString("%1").arg(item->counts()[index], 0, 'f', 5) << "\",\""
<< QString("%1").arg(item->stdmeans().value(index, 0.0f), 0, 'f', 5) << "\",\""
<< QString("%1").arg(item->stdvariances().value(index, 0.0f), 0, 'f', 5) <<"\"]";
} else if (item->counts()[index] == 0) {
// if count is 0 don't write it
stream << ConstructNameNumberString(QString("\t\t\t\""), name,
QString("\":\""), item->metrics()[index], QString("\""));
} else {
stream << ConstructNameNumberNumberString(QString("\t\t\t\""), name,
QString("\":[\""), item->metrics()[index], QString("\",\""), item->counts()[index], QString("\"]"));
}
}
}
// if opendata lets put in the distributions for power, hr, cadence and speed
if (opendata) {
QString data = item->getText("Data","");
if (data.contains("P") || data.contains("H") || data.contains("S") || data.contains("C")) {
// get the cache -- may refresh if its out of data, so this might take a while ....
RideFileCache *cache = new RideFileCache(context, item->fileName, item->weight, NULL, false, true);
QList<RideFile::SeriesType> list;
if (data.contains("P")) list <<RideFile::watts;
if (data.contains("H")) list <<RideFile::hr;
if (data.contains("S")) list <<RideFile::kph;
if (data.contains("C")) list <<RideFile::cad;
// output distribution for each series
foreach(RideFile::SeriesType x, list) {
QVector<double> &array = cache->distributionArray(x);
int split= x==RideFile::cad ? 5 : 10; // set bin size to use
int div= x==RideFile::kph ? 10 : 1; // speed needs to be divided by 10 to get kph
// distribution arrays:
// power_dist_bins:[ n1, n2, n3 ... ],
// power_dist:[ p1, p2, p3 ... ]
//
// the bins are always 10w wide, so we aggregate the data from
// the ridefilecache before outputting the bin starts and values
QVector<int> bins, totals;
// lets aggregates
int count=0;
double total=0;
for(int i=0; i< array.count(); i++) {
// save value away
if (count==split) {
if (total > 0) { // need a value !
bins << i-split;
totals << total;
}
count=0;
total=0;
}
total += array[i];
count++;
}
// add last partial bin
if (total > 0 && count > 0) {
bins << array.count()-count;
totals << total;
}
// series name
QString type=RideFile::seriesName(x, true).toLower();
// now write the distribution and the bins
if (bins.count()) {
// totals
stream << ",\n\t\t\t\""<<type<<"_dist\":[" ;
for(int i=0; i < totals.count(); i++) {
stream<< QString("%1").arg(totals[i]);
if (i+1 != bins.count()) stream<<", ";
}
stream << " ]";
// bins
stream << ",\n\t\t\t\""<<type<<"_dist_bins\":[" ;
for(int i=0; i < bins.count(); i++) {
stream<< QString("%1").arg(bins[i] > 0 ? bins[i] / div : 0);
if (i+1 != bins.count()) stream<<", ";
}
stream << " ]";
}
}
// MMP - We want to try and keep some of the resolution as this is likely to be filtered
// and/or aggregated in some fashion by the user. So we choose to keep resolution
// high at short durations and low at long durations with the points at which the
// resolution degrades at some point after a physiological boundary.
//
// power_mmp - the mean max values
// power_mmp_secs - the mean max durations
if (data.contains("P")) {
QVector<double> &array = cache->meanMaxArray(RideFile::watts);
if (array.count() > 0) {
// power_mmp
stream << ",\n\t\t\t\"power_mmp\":[" ;
for (int i=0; i<mmp_durations.count() && mmp_durations[i] < array.count(); i++) {
if (i>0) stream << ", "; // for all but first value add comma
stream<< QString("%1").arg(array[mmp_durations[i]], 0, 'f', 0);
}
stream << " ]";
// power_mmp_secs
stream << ",\n\t\t\t\"power_mmp_secs\":[" ;
for (int i=0; i<mmp_durations.count() && mmp_durations[i] < array.count(); i++) {
if (i>0) stream << ", "; // for all but first value add comma
stream<< QString("%1").arg(mmp_durations[i]);
}
stream << " ]";
}
}
delete cache;
}
}
stream << "\n\t\t}";
// pre-loaded metadata - don't send to OpenData
if (!opendata && item->metadata().count()) {
stream << ",\n\t\t\"TAGS\":{\n";
QMap<QString,QString>::const_iterator i;
for (i=item->metadata().constBegin(); i != item->metadata().constEnd(); i++) {
stream << "\t\t\t\"" << i.key() << "\":\"" << protect(i.value()) << "\"";
if (std::next(i) != item->metadata().constEnd()) stream << ",\n";
else stream << "\n";
}
// end of the tags
stream << "\n\t\t}";
}
// xdata definitions
if (item->xdata().count()) {
stream << ",\n\t\t\"XDATA\":{\n";
QMap<QString, QStringList>::const_iterator i;
for (i=item->xdata().constBegin(); i != item->xdata().constEnd(); i++) {
stream << "\t\t\t\"" << i.key() << "\":[ ";
bool first=true;
foreach(QString x, i.value()) {
if (!first) {
stream << ", ";
}
stream << "\"" << protect(x) << "\"";
first=false;
}
if (std::next(i) != item->xdata().constEnd()) stream << "],\n";
else stream << "]\n";
}
// end of the xdata
stream << "\n\t\t}";
}
// intervals - but not for opendata
if (!opendata && item->intervals().count()) {
stream << ",\n\t\t\"INTERVALS\":[\n";
bool firstInterval = true;
foreach(IntervalItem *interval, item->intervals()) {
// comma separate
if (!firstInterval) stream << ",\n";
firstInterval = false;
stream << "\t\t\t{\n";
// interval main data
stream << "\t\t\t\"name\":\"" << protect(interval->name) <<"\",\n";
stream << "\t\t\t\"start\":\"" << interval->start <<"\",\n";
stream << "\t\t\t\"stop\":\"" << interval->stop <<"\",\n";
stream << "\t\t\t\"startKM\":\"" << interval->startKM <<"\",\n";
stream << "\t\t\t\"stopKM\":\"" << interval->stopKM <<"\",\n";
stream << "\t\t\t\"type\":\"" << static_cast<int>(interval->type) <<"\",\n";
stream << "\t\t\t\"test\":\"" << (interval->test ? "true" : "false") <<"\",\n";
stream << "\t\t\t\"color\":\"" << interval->color.name() <<"\",\n";
// routes have a segment identifier
if (interval->type == RideFileInterval::ROUTE) {
stream << "\t\t\t\"route\":\"" << interval->route.toString() <<"\",\n"; // last one no ',\n' see METRICS below..
}
stream << "\t\t\t\"seq\":\"" << interval->displaySequence <<"\""; // last one no ',\n' see METRICS below..
// check if we have any non-zero metrics
bool hasMetrics=false;
foreach(double v, interval->metrics()) {
if (v > 0.00f || v < 0.00f) {
hasMetrics=true;
break;
}
}
if (hasMetrics) {
stream << ",\n\n\t\t\t\"METRICS\":{\n";
bool firstMetric = true;
for(int i=0; i<factory.metricCount(); i++) {
QString name = factory.metricName(i);
int index = factory.rideMetric(name)->index();
// don't output 0 values, they're set to 0 by default
// unless aggregateZero indicates the count is relevant
if ((interval->metrics()[index] > 0.00f || interval->metrics()[index] < 0.00f) ||
(item->metrics()[index] == 0.00f && item->counts()[i] > 1.0 && factory.rideMetric(name)->aggregateZero())) {
if (!firstMetric) stream << ",\n";
firstMetric = false;
if (interval->stdmeans().value(index, 0.0f) || interval->stdvariances().value(index, 0.0f)) {
stream << "\t\t\t\t\"" << name << "\": [ \"" << QString("%1").arg(interval->metrics()[index], 0, 'f', 5) <<"\",\""
<< QString("%1").arg(interval->counts()[index], 0, 'f', 5) << "\",\""
<< QString("%1").arg(interval->stdmeans().value(index, 0.0f), 0, 'f', 5) << "\",\""
<< QString("%1").arg(interval->stdvariances().value(index, 0.0f), 0, 'f', 5) <<"\"]";
// if count is 0 don't write it
} else if (interval->counts()[index] == 0) {
stream << ConstructNameNumberString(QString("\t\t\t\""), name,
QString("\":\""), interval->metrics()[index], QString("\""));
} else {
stream << ConstructNameNumberNumberString(QString("\t\t\t\""), name,
QString("\":[\""), interval->metrics()[index], QString("\",\""), interval->counts()[index], QString("\"]"));
}
}
}
stream << "\n\t\t\t\t}";
}
// endof interval
stream << "\n\t\t\t}";
}
// end of intervals
stream <<"\n\t\t]";
}
// end of the ride
stream << "\n\t}";
}
stream << "\n ]\n}";
rideDB.close();
}
}
#ifdef GC_WANT_HTTP
#include "RideMetadata.h"
void
APIWebService::listRides(QString athlete, HttpRequest &request, HttpResponse &response)
{
listRideSettings settings;
// the ride db
QString ridedb = QString("%1/%2/cache/rideDB.json").arg(home.absolutePath()).arg(athlete);
QFile rideDB(ridedb);
// list activities and associated metrics
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
// not known..
if (!rideDB.exists()) {
response.setStatus(404);
response.write("malformed URL or unknown athlete.\n");
return;
}
// intervals or rides?
QString intervalsp = request.getParameter("intervals");
if (intervalsp.toUpper() == "TRUE") settings.intervals = true;
else settings.intervals = false;
// set user data
response.setUserData(&settings);
// write headings
const RideMetricFactory &factory = RideMetricFactory::instance();
QVector<const RideMetric *> indexed(factory.metricCount());
// get metrics indexed in same order as the array
foreach(QString name, factory.allMetrics()) {
const RideMetric *m = factory.rideMetric(name);
indexed[m->index()] = m;
}
// was the metric parameter passed?
QString metrics(request.getParameter("metrics"));
// do all ?
bool nometrics = false;
QStringList wantedNames;
if (metrics != "") wantedNames = metrics.split(",");
// write headings
response.bwrite("date, time, filename");
// don't want metrics, so do it fast by traversing the ride directory
if (wantedNames.count() == 1 && wantedNames[0].toUpper() == "NONE") nometrics = true;
// if intervals, add interval name
if (settings.intervals == true) response.bwrite(", interval name, interval type");
// get metadata definitions into settings
QString metadata = request.getParameter("metadata");
bool nometa = true;
if (metadata.toUpper() != "NONE" && metadata != "") {
// first lets read in meta config
QString metaConfig = home.canonicalPath()+"/metadata.xml";
if (!QFile(metaConfig).exists()) metaConfig = ":/xml/metadata.xml";
// params to readXML - we ignore them
QList<KeywordDefinition> keywordDefinitions;
QString colorfield;
QList<DefaultDefinition> defaultDefinitions;
RideMetadata::readXML(metaConfig, keywordDefinitions, settings.metafields, colorfield, defaultDefinitions);
SpecialFields sp;
// what is being asked for ?
if (metadata.toUpper() == "ALL") {
// output all metadata
foreach(FieldDefinition field, settings.metafields) {
// don't do metric overrides !
if(!sp.isMetric(field.name)) settings.metawanted << field.name;
}
} else {
// selected fields - check they exist !
QStringList meta = metadata.split(",");
foreach(QString field, meta) {
// don't do metric overrides !
if(sp.isMetric(field)) continue;
// does it exist ?
QString lookup = field;
lookup.replace("_", " ");
foreach(FieldDefinition field, settings.metafields) {
if (field.name == lookup) settings.metawanted << field.name;
}
}
}
// we found some?
if(settings.metawanted.count()) nometa = false;
}
// list 'em by reading the ride cache from disk
if ((nometa == false || nometrics == false) && settings.intervals == false) {
int i=0;
foreach(const RideMetric *m, indexed) {
i++;
// if limited don't do limited headings
QString underscored = m->name().replace(" ","_");
if (wantedNames.count() && !wantedNames.contains(underscored)) continue;
if (m->name().startsWith("BikeScore"))
response.bwrite(", BikeScore");
else {
response.bwrite(", ");
response.bwrite(underscored.toLocal8Bit());
}
// index of wanted metrics
settings.wanted << (i-1);
}
// do we want metadata too ?
foreach(QString meta, settings.metawanted) {
meta.replace(" ", "_");
response.bwrite(", \"");
response.bwrite(meta.toLocal8Bit());
response.bwrite("\"");
}
response.bwrite("\n");
// parse the rideDB and write a line for each entry
if (rideDB.exists() && rideDB.open(QFile::ReadOnly)) {
// ok, lets read it in
QTextStream stream(&rideDB);
#if QT_VERSION < 0x060000
stream.setCodec("UTF-8");
#endif
// Read the entire file into a QString -- we avoid using fopen since it
// doesn't handle foreign characters well. Instead we use QFile and parse
// from a QString
QString contents = stream.readAll();
rideDB.close();
// create scanner context for reentrant parsing
RideDBContext *jc = new RideDBContext;
jc->cache = NULL;
jc->api = this;
jc->response = &response;
jc->request = &request;
jc->old = false;
// clean item
jc->item.path = home.absolutePath() + "/activities";
jc->item.context = NULL;
jc->item.isstale = jc->item.isdirty = jc->item.isedit = false;
RideDBlex_init(&scanner);
// inform the parser/lexer we have a new file
RideDB_setString(contents, scanner);
// setup
jc->errors.clear();
// parse it
RideDBparse(jc);
// clean up
RideDBlex_destroy(scanner);
// regardless of errors we're done !
delete jc;
}
} else {
// honour the since parameter
QString sincep(request.getParameter("since"));
QDate since(1900,01,01);
if (sincep != "") since = QDate::fromString(sincep,"yyyy/MM/dd");
// before parameter
QString beforep(request.getParameter("before"));
QDate before(3000,01,01);
if (beforep != "") before = QDate::fromString(beforep,"yyyy/MM/dd");
// fast list of rides by traversing the directory
response.bwrite("\n"); // headings have no metric columns
// This will read the user preferences and change the file list order as necessary:
QFlags<QDir::Filter> spec = QDir::Files;
QStringList names;
names << "*"; // anything
// loop through files, make sure in time range wanted
QDir activities(home.absolutePath() + "/" + athlete + "/activities");
foreach(QString name, activities.entryList(names, spec, QDir::Name)) {
// parse it into date and time
QDateTime dateTime;
if (!RideFile::parseRideFileName(name, &dateTime)) continue;
// in range?
if (dateTime.date() < since || dateTime.date() > before) continue;
// is it a backup ?
if (name.endsWith(".bak")) continue;
// out a line
response.bwrite(dateTime.date().toString("yyyy/MM/dd").toLocal8Bit());
response.bwrite(", ");
response.bwrite(dateTime.time().toString("hh:mm:ss").toLocal8Bit());;
response.bwrite(", ");
response.bwrite(name.toLocal8Bit());
response.bwrite("\n");
}
}
response.flush();
}
#endif