/* * Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net), * Justin F. Knotzke (jknotzke@shampoo.ca) * * 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 "CsvRideFile.h" #include #include #include // for std::sort #include #include "math.h" #define MILES_TO_KM 1.609344 static int csvFileReaderRegistered = RideFileFactory::instance().registerReader("csv", new CsvFileReader()); RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const { QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive); QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive); bool metric; // TODO: a more robust regex for ergomo files // i don't have an example with english headers // the ergomo CSV has two rows of headers. units are on the second row. /* ZEIT,STRECKE,POWER,RPM,SPEED,PULS,HÖHE,TEMP,INTERVAL,PAUSE L_SEC,KM,WATT,RPM,KM/H,BPM,METER,°C,NUM,SEC */ QRegExp ergomoCSV("(ZEIT|STRECKE)", Qt::CaseInsensitive); bool ergomo = false; int unitsHeader = 1; int total_pause = 0; // TODO: with all these formats, should the logic change to a switch/case structure? // The iBike format CSV file has five lines of headers (data begins on line 6) // starting with: /* iBike,8,english 2008,8,8,6,32,52 {Various configuration data, recording interval at line[4][4]} Speed (mph),Wind Speed (mph),Power (W),Distance (miles),Cadence (RPM),Heartrate (BPM),Elevation (feet),Hill slope (%),Internal,Internal,Internal,DFPM Power,Latitude,Longitude */ // Modified the regExp string to allow for 2-digit version numbers - 23 Mar 2009, thm QRegExp iBikeCSV("iBike,\\d\\d?,[a-z]+", Qt::CaseInsensitive); bool iBike = false; int recInterval; if (!file.open(QFile::ReadOnly)) { errors << ("Could not open ride file: \"" + file.fileName() + "\""); return NULL; } int lineno = 1; QTextStream is(&file); RideFile *rideFile = new RideFile(); while (!is.atEnd()) { // the readLine() method doesn't handle old Macintosh CR line endings // this workaround will load the the entire file if it has CR endings // then split and loop through each line // otherwise, there will be nothing to split and it will read each line as expected. QString linesIn = is.readLine(); QStringList lines = linesIn.split('\r'); // workaround for empty lines if(lines.isEmpty()) { lineno++; continue; } for (int li = 0; li < lines.size(); ++li) { QString line = lines[li]; if (lineno == 1) { if (ergomoCSV.indexIn(line) != -1) { ergomo = true; rideFile->setDeviceType("Ergomo CSV"); unitsHeader = 2; ++lineno; continue; } else { if(iBikeCSV.indexIn(line) != -1) { iBike = true; rideFile->setDeviceType("iBike CSV"); unitsHeader = 5; ++lineno; continue; } rideFile->setDeviceType("PowerTap CSV"); } } if (iBike && lineno == 4) { // this is the line with the iBike configuration data // recording interval is in the [4] location (zero-based array) // the trailing zeroes in the configuration area seem to be causing an error // the number is in the format 5.000000 recInterval = (int)line.section(',',4,4).toDouble(); } if (lineno == unitsHeader) { if (metricUnits.indexIn(line) != -1) metric = true; else if (englishUnits.indexIn(line) != -1) metric = false; else { errors << "Can't find units in first line: \"" + line + "\" of file \"" + file.fileName() + "\"."; delete rideFile; file.close(); return NULL; } } else if (lineno > unitsHeader) { double minutes,nm,kph,watts,km,cad,hr; int interval; int pause; if (!ergomo && !iBike) { minutes = line.section(',', 0, 0).toDouble(); nm = line.section(',', 1, 1).toDouble(); kph = line.section(',', 2, 2).toDouble(); watts = line.section(',', 3, 3).toDouble(); km = line.section(',', 4, 4).toDouble(); cad = line.section(',', 5, 5).toDouble(); hr = line.section(',', 6, 6).toDouble(); interval = line.section(',', 7, 7).toInt(); if (!metric) { km *= MILES_TO_KM; kph *= MILES_TO_KM; } } else if (iBike) { // this must be iBike // can't find time as a column. // will we have to extrapolate based on the recording interval? // reading recording interval from config data in ibike csv file minutes = (recInterval * lineno - unitsHeader)/60.0; nm = NULL; //no torque kph = line.section(',', 0, 0).toDouble(); watts = line.section(',', 2, 2).toDouble(); km = line.section(',', 3, 3).toDouble(); cad = line.section(',', 4, 4).toDouble(); hr = line.section(',', 5, 5).toDouble(); interval = NULL; //not provided? if (!metric) { km *= MILES_TO_KM; kph *= MILES_TO_KM; } } else { // for ergomo formatted CSV files minutes = line.section(',', 0, 0).toDouble() + total_pause; km = line.section(',', 1, 1).toDouble(); watts = line.section(',', 2, 2).toDouble(); cad = line.section(',', 3, 3).toDouble(); kph = line.section(',', 4, 4).toDouble(); hr = line.section(',', 5, 5).toDouble(); interval = line.section(',', 8, 8).toInt(); pause = line.section(',', 9, 9).toInt(); total_pause += pause; nm = NULL; // torque is not provided in the Ergomo file // the ergomo records the time in whole seconds // RECORDING INT. 1, 2, 5, 10, 15 or 30 per sec // Time is *always* perfectly sequential. To find pauses, // you need to read the PAUSE column. minutes = minutes/60.0; if (!metric) { km *= MILES_TO_KM; kph *= MILES_TO_KM; } } // PT reports no data as watts == -1. if (watts == -1) watts = 0; rideFile->appendPoint(minutes * 60.0, cad, hr, km, kph, nm, watts, interval); } ++lineno; } } file.close(); // To estimate the recording interval, take the median of the // first 1000 samples and round to nearest millisecond. int n = rideFile->dataPoints().size(); n = qMin(n, 1000); if (n >= 2) { double *secs = new double[n-1]; for (int i = 0; i < n-1; ++i) { double now = rideFile->dataPoints()[i]->secs; double then = rideFile->dataPoints()[i+1]->secs; secs[i] = then - now; } std::sort(secs, secs + n - 1); int mid = n / 2 - 1; double recint = round(secs[mid] * 1000.0) / 1000.0; rideFile->setRecIntSecs(recint); } // less than 2 data points is not a valid ride file else { errors << "Insufficient valid data in file \"" + file.fileName() + "\"."; delete rideFile; file.close(); return NULL; } QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_" "(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$"); if (rideTime.indexIn(file.fileName()) >= 0) { QDateTime datetime(QDate(rideTime.cap(1).toInt(), rideTime.cap(2).toInt(), rideTime.cap(3).toInt()), QTime(rideTime.cap(4).toInt(), rideTime.cap(5).toInt(), rideTime.cap(6).toInt())); rideFile->setStartTime(datetime); } return rideFile; }