mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
609 lines
21 KiB
C++
609 lines
21 KiB
C++
/*
|
|
* Copyright 2015 (c) 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 "APIWebService.h"
|
|
|
|
#include "Settings.h"
|
|
#include "GcUpgrade.h"
|
|
#include "RideDB.h"
|
|
|
|
#include "RideFile.h"
|
|
#include "RideFileCache.h"
|
|
#include "CsvRideFile.h"
|
|
|
|
#include "Zones.h"
|
|
#include "HrZones.h"
|
|
#include "PaceZones.h"
|
|
|
|
#include <QTemporaryFile>
|
|
#include <QFile>
|
|
|
|
void
|
|
APIWebService::service(HttpRequest &request, HttpResponse &response)
|
|
{
|
|
// remove trailing '/' from request, just to be consistent
|
|
QString fullPath = request.getPath();
|
|
while (fullPath.endsWith("/")) fullPath.chop(1);
|
|
|
|
// get the paths, strip empty stuff
|
|
QStringList paths = QString(request.getPath()).split("/");
|
|
while (paths.count() && paths[paths.count()-1] == "") paths.removeLast();
|
|
while (paths.count() && paths[0] == "") paths.removeFirst();
|
|
|
|
// we don't have a fave icon
|
|
if (paths.count() && paths[0] == "favicon.ico") return;
|
|
|
|
// ROOT PATH RETURNS A LIST OF ATHLETES
|
|
if (paths.count() == 0) {
|
|
listAthletes(request, response); // return csv list of all athlete and their characteristics
|
|
return;
|
|
}
|
|
|
|
// Call to retreive athlete data, downstream will resolve
|
|
// which functions to call for different data requests
|
|
athleteData(paths, request, response);
|
|
}
|
|
|
|
void
|
|
APIWebService::athleteData(QStringList &paths, HttpRequest &request, HttpResponse &response)
|
|
{
|
|
|
|
// check we have an athlete and it is valid
|
|
if (paths.count() == 0) {
|
|
|
|
response.setStatus(404); // malformed URL
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
response.write("missing athlete.");
|
|
return;
|
|
} else {
|
|
QFile ridedb(home.absolutePath() + "/" + paths[0] + "/cache/rideDB.json");
|
|
if (!ridedb.exists()) {
|
|
response.setStatus(404); // malformed URL
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
response.write("unknown athlete " + paths[0].toLocal8Bit());
|
|
return;
|
|
}
|
|
}
|
|
if (paths.count() == 1) {
|
|
|
|
// LIST ACTIVITIES FOR ATHLETE
|
|
// http://localhost:12021/athlete
|
|
listRides(paths[0], request, response);
|
|
return;
|
|
|
|
} else if (paths.count() == 2) {
|
|
|
|
QString athlete = paths[0];
|
|
paths.removeFirst();
|
|
|
|
// GET ZONES
|
|
// http://localhost:12021/athlete/zones
|
|
if (paths[0] == "zones") {
|
|
listZones(athlete, paths, request, response);
|
|
return;
|
|
}
|
|
|
|
} else if (paths.count() == 3) {
|
|
|
|
QString athlete = paths[0];
|
|
paths.removeFirst();
|
|
|
|
// GET ACTIVITY
|
|
// http://localhost:12021/athlete/activity/filename
|
|
// optional query parameters:
|
|
// ?format=json (default)
|
|
// ?format=<xx> xx = one of (csv, tcx, pwx)
|
|
if (paths[0] == "activity") {
|
|
|
|
paths.removeFirst();
|
|
listActivity(athlete, paths, request, response);
|
|
return;
|
|
}
|
|
|
|
// GET MMP
|
|
if (paths[0] == "meanmax") {
|
|
|
|
// http://localhost:12021/athlete/meanmax/filename
|
|
// optional query parameter:
|
|
// ?series=watts (default)
|
|
// ?series=<xx> xx= one of (cad, speed, vam, NP, xPower, nm)
|
|
// http://localhost:12021/athlete/meanmax/bests
|
|
// optional query parameter:
|
|
// ?series=watts (default)
|
|
// ?series=<xx> xx=1 of (cad, speed, vam, NP, xPower, nm)
|
|
paths.removeFirst();
|
|
listMMP(athlete, paths, request, response);
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
// GET HERE ITS BAD!
|
|
response.setStatus(404); // malformed URL
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
response.write("malformed url");
|
|
}
|
|
|
|
void
|
|
APIWebService::listAthletes(HttpRequest &, HttpResponse &response)
|
|
{
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
|
|
// This will read the user preferences and change the file list order as necessary:
|
|
QFlags<QDir::Filter> spec = QDir::Dirs;
|
|
QStringList names;
|
|
names << "*"; // anything
|
|
|
|
response.write("name,dob,weight,height,sex\n");
|
|
foreach(QString name, home.entryList(names, spec, QDir::Name)) {
|
|
|
|
// sure fire sign the athlete has been upgraded to post 3.2 and not some
|
|
// random directory full of other things & check something basic is set
|
|
QString ridedb = home.absolutePath() + "/" + name + "/cache/rideDB.json";
|
|
if (QFile(ridedb).exists() && appsettings->cvalue(name, GC_SEX, "") != "") {
|
|
// we got one
|
|
QString line = name;
|
|
line += ", " + appsettings->cvalue(name, GC_DOB).toDate().toString("yyyy/MM/dd");
|
|
line += ", " + QString("%1").arg(appsettings->cvalue(name, GC_WEIGHT).toDouble());
|
|
line += ", " + QString("%1").arg(appsettings->cvalue(name, GC_HEIGHT).toDouble());
|
|
line += (appsettings->cvalue(name, GC_SEX).toInt() == 0) ? ", Male" : ", Female";
|
|
line += "\n";
|
|
|
|
// out a line
|
|
response.write(line.toLocal8Bit());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
APIWebService::writeRideLine(RideItem &item, HttpRequest *request, HttpResponse *response)
|
|
{
|
|
|
|
// 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");
|
|
|
|
// in range?
|
|
if (item.dateTime.date() < since) return;
|
|
if (item.dateTime.date() > before) return;
|
|
|
|
// are we doing rides or intervals?
|
|
listRideSettings *settings = static_cast<listRideSettings *>(response->userData());
|
|
|
|
if (settings->intervals == true) {
|
|
|
|
// loop through all available intervals for this ride item
|
|
foreach(IntervalItem *interval, item.intervals()){
|
|
|
|
// date, time, filename
|
|
response->bwrite(item.dateTime.date().toString("yyyy/MM/dd").toLocal8Bit());
|
|
response->bwrite(", ");
|
|
response->bwrite(item.dateTime.time().toString("hh:mm:ss").toLocal8Bit());;
|
|
response->bwrite(", ");
|
|
response->bwrite(item.fileName.toLocal8Bit());
|
|
|
|
// now the interval name and type
|
|
response->bwrite(", \"");
|
|
response->bwrite(interval->name.toLocal8Bit());
|
|
response->bwrite("\", ");
|
|
response->bwrite(QString("%1").arg(static_cast<int>(interval->type)).toLocal8Bit());
|
|
|
|
// essentially the same as below .. cut and paste (refactor?XXX)
|
|
if (settings->wanted.count()) {
|
|
// specific metrics
|
|
foreach(int index, settings->wanted) {
|
|
double value = interval->metrics()[index];
|
|
response->bwrite(",");
|
|
response->bwrite(QString("%1").arg(value, 'f').simplified().toLocal8Bit());
|
|
}
|
|
} else {
|
|
|
|
// all metrics...
|
|
foreach(double value, interval->metrics()) {
|
|
response->bwrite(",");
|
|
response->bwrite(QString("%1").arg(value, 'f').simplified().toLocal8Bit());
|
|
}
|
|
}
|
|
response->bwrite("\n");
|
|
}
|
|
|
|
} else {
|
|
|
|
// date, time, filename
|
|
response->bwrite(item.dateTime.date().toString("yyyy/MM/dd").toLocal8Bit());
|
|
response->bwrite(",");
|
|
response->bwrite(item.dateTime.time().toString("hh:mm:ss").toLocal8Bit());;
|
|
response->bwrite(",");
|
|
response->bwrite(item.fileName.toLocal8Bit());
|
|
|
|
if (settings->wanted.count()) {
|
|
// specific metrics
|
|
foreach(int index, settings->wanted) {
|
|
double value = item.metrics()[index];
|
|
response->bwrite(",");
|
|
response->bwrite(QString("%1").arg(value, 'f').simplified().toLocal8Bit());
|
|
}
|
|
} else {
|
|
|
|
// all metrics...
|
|
foreach(double value, item.metrics()) {
|
|
response->bwrite(",");
|
|
response->bwrite(QString("%1").arg(value, 'f').simplified().toLocal8Bit());
|
|
}
|
|
}
|
|
|
|
// all the metadata asked for
|
|
foreach(QString name, settings->metawanted) {
|
|
QString text = item.getText(name,"");
|
|
text.replace("\"","'"); // don't use double quotes...
|
|
text.replace("\n","\\n"); // newlines
|
|
text.replace("\r","\\r"); // carriage returns
|
|
text.replace("\t","\\t"); // tabs
|
|
|
|
response->bwrite(",\"");
|
|
response->bwrite(text.toLocal8Bit());
|
|
response->bwrite("\"");
|
|
}
|
|
|
|
response->bwrite("\n");
|
|
}
|
|
}
|
|
|
|
void
|
|
APIWebService::listActivity(QString athlete, QStringList paths, HttpRequest &request, HttpResponse &response)
|
|
{
|
|
// does it exist ?
|
|
QString filename = QString("%1/%2/activities/%3").arg(home.absolutePath()).arg(athlete).arg(paths[0]);
|
|
|
|
QString contents;
|
|
QFile file(filename);
|
|
if (file.exists() && file.open(QFile::ReadOnly | QFile::Text)) {
|
|
|
|
// close as we will open properly below
|
|
file.close();
|
|
|
|
// what format to use ?
|
|
QString format(request.getParameter("format"));
|
|
if (format == "") {
|
|
|
|
// if not passed in the URL then is content type
|
|
// caller can accept listed in the header?
|
|
// there is probably a more complete way of handling
|
|
// wildcards etc, but the user can always force via
|
|
// the format parameter in the URL
|
|
foreach(QByteArray accepts, request.getHeaders("Accept")) {
|
|
if (accepts == "application/json") format="json";
|
|
if (accepts == "text/csv") format="csv";
|
|
if (accepts == "application/vnd.garmin.tcx") format="tcx";
|
|
if (accepts == "application/vnd.trainingpeaks.pwx") format="pwx";
|
|
if (accepts == "application/xml" || accepts == "text/xml") format="tcx";
|
|
if (format != "") break;
|
|
}
|
|
}
|
|
|
|
// default to json
|
|
if (format == "") format = "json";
|
|
|
|
// lets go with tcx/pwx as xml, full csv (not powertap) and GC json
|
|
QStringList formats;
|
|
formats << "tcx"; // garmin training centre
|
|
formats << "csv"; // full csv list (not powertap)
|
|
formats << "json"; // gc json
|
|
formats << "pwx"; // gc json
|
|
|
|
// unsupported format
|
|
if (!formats.contains(format)) {
|
|
response.setStatus(500);
|
|
response.write("unsupported format; we support:");
|
|
foreach(QString fmt, formats) {
|
|
response.write(" ");
|
|
response.write(fmt.toLocal8Bit());
|
|
}
|
|
response.write("\r\n");
|
|
return;
|
|
} else {
|
|
|
|
// set the content type appropriately
|
|
if (format == "tcx") response.setHeader("Content-Type", "application/vnd.garmin.tcx+xml; charset=ISO-8859-1");
|
|
if (format == "csv") response.setHeader("Content-Type", "text/csv; charset=ISO-8859-1");
|
|
if (format == "json") response.setHeader("Content-Type", "application/json; charset=ISO-8859-1");
|
|
if (format == "pwx") response.setHeader("Content-Type", "application/vnd.trainingpeaks.pwx+xml; charset=ISO-8859-1");
|
|
}
|
|
|
|
// lets read the file in as a ridefile
|
|
QStringList errors;
|
|
RideFile *f = RideFileFactory::instance().openRideFile(NULL, file, errors);
|
|
|
|
// error reading (!)
|
|
if (f == NULL) {
|
|
response.setStatus(500);
|
|
foreach(QString error, errors) {
|
|
response.write(error.toLocal8Bit());
|
|
response.write("\r\n");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// write out to a temporary file in
|
|
// the format requested
|
|
bool success;
|
|
QTemporaryFile tempfile; // deletes file when goes out of scope
|
|
QString tempname;
|
|
if (tempfile.open()) tempname = tempfile.fileName();
|
|
else {
|
|
response.setStatus(500);
|
|
response.write("error opening temporary file");
|
|
return;
|
|
}
|
|
QFile out(tempname);
|
|
|
|
if (format == "csv") {
|
|
CsvFileReader writer;
|
|
success = writer.writeRideFile(NULL, f, out, CsvFileReader::gc);
|
|
} else {
|
|
success = RideFileFactory::instance().writeRideFile(NULL, f, out, format);
|
|
}
|
|
|
|
if (success) {
|
|
|
|
// read in the whole thing
|
|
out.open(QFile::ReadOnly | QFile::Text);
|
|
QTextStream in(&out);
|
|
in.setCodec ("UTF-8");
|
|
contents = in.readAll();
|
|
out.close();
|
|
|
|
// write back in one hit
|
|
response.write(contents.toLocal8Bit(), true);
|
|
return;
|
|
|
|
} else {
|
|
response.setStatus(500);
|
|
response.write("unable to write output, internal error.\n");
|
|
return;
|
|
}
|
|
|
|
} else {
|
|
|
|
// nope?
|
|
response.setStatus(404);
|
|
response.write("file not found");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
APIWebService::listMMP(QString athlete, QStringList paths, HttpRequest &request, HttpResponse &response)
|
|
{
|
|
// list activities and associated metrics
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
|
|
// what series do we want ?
|
|
QString seriesp = request.getParameter("series");
|
|
if (seriesp == "") seriesp = "watts";
|
|
RideFile::SeriesType series;
|
|
|
|
// what asked for ?
|
|
if (seriesp == "hr") series = RideFile::hr;
|
|
else if (seriesp == "cad") series = RideFile::cad;
|
|
else if (seriesp == "speed") series = RideFile::kph;
|
|
else if (seriesp == "watts") series = RideFile::watts;
|
|
else if (seriesp == "vam") series = RideFile::vam;
|
|
else if (seriesp == "NP") series = RideFile::NP;
|
|
else if (seriesp == "xPower") series = RideFile::xPower;
|
|
else if (seriesp == "nm") series = RideFile::nm;
|
|
else {
|
|
|
|
// unknown series
|
|
response.setStatus(500);
|
|
response.write("unknown series requested.\n");
|
|
return;
|
|
}
|
|
|
|
QString filename=paths[0];
|
|
|
|
if (paths[0] == "bests") {
|
|
|
|
// header
|
|
response.bwrite("secs, ");
|
|
response.bwrite(seriesp.toLocal8Bit());
|
|
response.bwrite("\n");
|
|
|
|
// 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");
|
|
|
|
int secs=0;
|
|
foreach(float value, RideFileCache::meanMaxFor(home.absolutePath() + "/" + athlete + "/cache", series, since, before)) {
|
|
if (secs >0) response.bwrite(QString("%1, %2\n").arg(secs).arg(value).toLocal8Bit());
|
|
secs++;
|
|
}
|
|
|
|
|
|
} else {
|
|
QString CPXfilename = home.absolutePath() + "/" + athlete + "/cache/" + QFileInfo(filename).completeBaseName() + ".cpx";
|
|
|
|
// header
|
|
response.bwrite("secs, ");
|
|
response.bwrite(seriesp.toLocal8Bit());
|
|
response.bwrite("\n");
|
|
|
|
if (QFileInfo(CPXfilename).exists()) {
|
|
int secs=0;
|
|
foreach(float value, RideFileCache::meanMaxFor(CPXfilename, series)) {
|
|
if (secs >0) response.bwrite(QString("%1, %2\n").arg(secs).arg(value).toLocal8Bit());
|
|
secs++;
|
|
}
|
|
}
|
|
response.flush();
|
|
}
|
|
}
|
|
|
|
void
|
|
APIWebService::listZones(QString athlete, QStringList, HttpRequest &request, HttpResponse &response)
|
|
{
|
|
// list activities and associated metrics
|
|
response.setHeader("Content-Type", "text; charset=ISO-8859-1");
|
|
|
|
// what zones we support
|
|
QStringList zonelist;
|
|
zonelist << "power" << "hr" << "pace" << "swimpace";
|
|
|
|
// what series do we want ?
|
|
QString zonesFor = request.getParameter("for");
|
|
if (zonesFor == "") zonesFor = "power";
|
|
else if (!zonelist.contains(zonesFor)) {
|
|
response.setStatus(404);
|
|
response.write("unknown zones type; one of power, hr, pace and swimpace expected.\n");
|
|
return;
|
|
}
|
|
|
|
// power zones
|
|
if (zonesFor == "power") {
|
|
|
|
// Power Zones
|
|
QFile zonesFile(home.absolutePath() + "/" + athlete + "/config/power.zones");
|
|
if (zonesFile.exists()) {
|
|
Zones *zones = new Zones;
|
|
if (zones->read(zonesFile)) {
|
|
|
|
// success - write out
|
|
response.write("date, cp, w', pmax\n");
|
|
for(int i=0; i<zones->getRangeSize(); i++) {
|
|
response.write(
|
|
QString("%1, %2, %3, %4\n")
|
|
.arg(zones->getStartDate(i).toString("yyyy/MM/dd"))
|
|
.arg(zones->getCP(i))
|
|
.arg(zones->getWprime(i))
|
|
.arg(zones->getPmax(i))
|
|
.toLocal8Bit()
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// drop here on fail
|
|
response.setStatus(500);
|
|
response.write("unable to read/parse the athlete's power.zones file.\n");
|
|
return;
|
|
}
|
|
|
|
// hr zones
|
|
if (zonesFor == "hr") {
|
|
|
|
// Zones
|
|
QFile zonesFile(home.absolutePath() + "/" + athlete + "/config/hr.zones");
|
|
if (zonesFile.exists()) {
|
|
HrZones *zones = new HrZones;
|
|
if (zones->read(zonesFile)) {
|
|
|
|
// success - write out
|
|
response.write("date, lthr, maxhr, rhr\n");
|
|
for(int i=0; i<zones->getRangeSize(); i++) {
|
|
response.write(
|
|
QString("%1, %2, %3, %4\n")
|
|
.arg(zones->getStartDate(i).toString("yyyy/MM/dd"))
|
|
.arg(zones->getLT(i))
|
|
.arg(zones->getMaxHr(i))
|
|
.arg(zones->getRestHr(i))
|
|
.toLocal8Bit()
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// drop here on fail
|
|
response.setStatus(500);
|
|
response.write("unable to read/parse the athlete's hr.zones file.\n");
|
|
return;
|
|
}
|
|
|
|
// pace zones
|
|
if (zonesFor == "pace") {
|
|
|
|
// Zones
|
|
QFile zonesFile(home.absolutePath() + "/" + athlete + "/config/run-pace.zones");
|
|
if (zonesFile.exists()) {
|
|
PaceZones *zones = new PaceZones;
|
|
if (zones->read(zonesFile)) {
|
|
|
|
// success - write out
|
|
response.write("date, CV\n");
|
|
for(int i=0; i<zones->getRangeSize(); i++) {
|
|
response.write(
|
|
QString("%1, %2\n")
|
|
.arg(zones->getStartDate(i).toString("yyyy/MM/dd"))
|
|
.arg(zones->getCV(i))
|
|
.toLocal8Bit()
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// drop here on fail
|
|
response.setStatus(500);
|
|
response.write("unable to read/parse the athlete's run-pace.zones file.\n");
|
|
return;
|
|
}
|
|
|
|
// swim pace zones
|
|
if (zonesFor == "swimpace") {
|
|
|
|
// Zones
|
|
QFile zonesFile(home.absolutePath() + "/" + athlete + "/config/swim-pace.zones");
|
|
if (zonesFile.exists()) {
|
|
PaceZones *zones = new PaceZones;
|
|
if (zones->read(zonesFile)) {
|
|
|
|
// success - write out
|
|
response.write("date, CV\n");
|
|
for(int i=0; i<zones->getRangeSize(); i++) {
|
|
response.write(
|
|
QString("%1, %2\n")
|
|
.arg(zones->getStartDate(i).toString("yyyy/MM/dd"))
|
|
.arg(zones->getCV(i))
|
|
.toLocal8Bit()
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// drop here on fail
|
|
response.setStatus(500);
|
|
response.write("unable to read/parse the athlete's swim-pace.zones file.\n");
|
|
return;
|
|
}
|
|
}
|