/* * 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 #include 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 = 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= one of (cad, speed, vam, NP, xPower, nm) // http://localhost:12021/athlete/meanmax/bests // optional query parameter: // ?series=watts (default) // ?series= 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 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(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(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; igetRangeSize(); 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; igetRangeSize(); 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; igetRangeSize(); 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; igetRangeSize(); 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; } }