/* * Copyright (c) 2011,2014 Damien Grauser (Damien.Grauser@pev-geneve.ch) * * 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 "Route.h" #include "Athlete.h" #include "MainWindow.h" #include "RideItem.h" #include "IntervalItem.h" #include "RouteParser.h" #include "RideFile.h" #include "GProgressDialog.h" #include #include #include #include #define tr(s) QObject::tr(s) #define pi 3.14159265358979323846 /* * RouteSegment * */ RouteSegment::RouteSegment() { _id = QUuid::createUuid(); // in case it isn't set yet } RouteSegment::RouteSegment(Routes *routes) : routes(routes) { _id = QUuid::createUuid(); // in case it isn't set yet } QString RouteSegment::getName() { return name; } void RouteSegment::setName(QString _name) { name = _name; } QList RouteSegment::getPoints() { return points; } int RouteSegment::addPoint(RoutePoint _point) { points.append(_point); return points.count(); } QList RouteSegment::getRides() { return rides; } int RouteSegment::addRide(RouteRide _ride) { rides.append(_ride); return rides.count(); } int RouteSegment::addRideForRideFile(const RideFile *ride, double start, double stop, double precision) { RouteRide _route = RouteRide(ride->startTime(), start, stop, precision); rides.append(_route); return rides.count(); } bool RouteSegment::parseRideFileName(Context *context, const QString &name, QString *notesFileName, QDateTime *dt) { static char rideFileRegExp[] = "^((\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)" "_(\\d\\d)_(\\d\\d)_(\\d\\d))\\.(.+)$"; QRegExp rx(rideFileRegExp); if (!rx.exactMatch(name)) return false; //assert(rx.captureCount() == 8); QDate date(rx.cap(2).toInt(), rx.cap(3).toInt(),rx.cap(4).toInt()); QTime time(rx.cap(5).toInt(), rx.cap(6).toInt(),rx.cap(7).toInt()); if ((! date.isValid()) || (! time.isValid())) { QMessageBox::warning(context->mainWindow, tr("Invalid Ride File Name"), tr("Invalid date/time in filename:\n%1\nSkipping file...").arg(name)); return false; } *dt = QDateTime(date, time); *notesFileName = rx.cap(1) + ".notes"; return true; } void RouteSegment::removeRideInRoute(RideFile* ride) { for (int n=0; n< this->getRides().count();n++) { const RouteRide* routeride = &getRides().at(n); if (ride && routeride->startTime == ride->startTime()) { rides.removeAt(n); } } } void RouteSegment::searchRouteInRide(RideFile* ride, bool freememory, QTextStream* out) { //*out << "searchRouteInRide " << ride->startTime().toString() << "\r\n"; bool candidate = false; // This RideFile is candidate double minimumprecision = 0.100; //100m double maximumprecision = 0.010; //10m double precision = -1; int diverge = 0; int lastpoint = -1; // Last point to match double start = -1, stop = -1; // Start and stop secs //foreach (RoutePoint routepoint, this->getPoints()) { for (int n=0; n< this->getPoints().count();n++) { RoutePoint routepoint = this->getPoints().at(n); bool present = false; RideFilePoint* point; for (int i=lastpoint+1; idataPoints().count();i++) { point = ride->dataPoints().at(i); double minimumdistance = -1; if (point->lat != 0 && point->lon !=0 && ceil(point->lat) != 180 && ceil(point->lon) != 180 && ceil(point->lat) != 540 && ceil(point->lon) != 540) { // Valid GPS value if (start == -1) { diverge = 0; // Calcul distance to route point double _dist = distance(routepoint.lat, routepoint.lon, point->lat, point->lon) ; minimumdistance = _dist; if (precision == -1 || _dist1) { // fare away from reference point i += 50; } else { if (_distdataPoints().count() && jdataPoints().at(j); if (nextpoint->lat != 0 && nextpoint->lon !=0 && ceil(nextpoint->lat) != 180 && ceil(nextpoint->lon) != 180) { double _nextdist = distance(routepoint.lat, routepoint.lon, nextpoint->lat, nextpoint->lon) ; if (minimumdistance ==-1 || _nextdistsecs; precision = 0; *out << "start time " << start << "\r\n"; } if (minimumdistance>precision) precision = minimumdistance; break; } else { *out << " WARNING route diverge at " << point->secs << "(" << i <<") after " << (point->secs-start)<< "secs for " << minimumdistance << "km " << routepoint.lat << "-" << routepoint.lon << "/" << point->lat << "-" << point->lon << "\r\n"; diverge++; if (diverge>2) { *out << " STOP route diverge at " << point->secs << "(" << i <<") after " << (point->secs-start)<< "secs for " << minimumdistance << "km " << routepoint.lat << "-" << routepoint.lon << "/" << point->lat << "-" << point->lon << "\r\n"; start = -1; //try to restart n = 0; } //present = true; } } } } stop = point->secs; if (!present) { *out << " Route not identified (distance " << precision << "km)\r\n"; break; } else { if (n == this->getPoints().count()-1){ // OK //Add the interval and continu search *out << " >>> Route identified in ride: " << name << " start: " << start << " stop: " << stop << " (distance " << precision << "km)\r\n"; this->addRideForRideFile(ride, start, stop, precision); candidate = true; *out << " search again..." << "\r\n"; start = -1; n=0; //break; } } } if (freememory && !candidate && ride) delete ride; // free memory - if needed } void RouteSegment::searchRouteInAllRides(Context* context) { qDebug() << "searchRouteInAllRides()"; // log of progress QFile log(context->athlete->home->logs().canonicalPath() + "/" + "routes.log"); log.open(QIODevice::ReadWrite); QTextStream out(&log); out << "SEARCH NEW ROUTE STARTS: " << QDateTime::currentDateTime().toString() + "\r\n"; QStringList filenames = RideFileFactory::instance().listRideFiles(context->athlete->home->activities()); QStringListIterator iterator(filenames); QStringList errors; // update statistics for ride files which are out of date // showing a progress bar as we go QTime elapsed; elapsed.start(); QString title = tr("Searching route"); GProgressDialog *bar = NULL; int processed=0; while (iterator.hasNext()) { QString name = iterator.next(); QFile file(context->athlete->home->activities().canonicalPath() + "/" + name); out << "Opening ride: " << name; RideFile *ride = NULL; // update progress bar long elapsedtime = elapsed.elapsed(); QString elapsedString = QString("%1:%2:%3").arg(elapsedtime/3600000,2) .arg((elapsedtime%3600000)/60000,2,10,QLatin1Char('0')) .arg((elapsedtime%60000)/1000,2,10,QLatin1Char('0')); // create the dialog if we need to show progress for long running uodate if ((elapsedtime > 2000) && bar == NULL) { bar = new GProgressDialog(title, 0, filenames.count(), context->mainWindow->init, context->mainWindow); bar->show(); // lets hide until elapsed time is > 2 seconds // lets make sure it goes to the center! QApplication::processEvents(); } if (bar) { QString title = tr("Searching route in all rides...\nElapsed: %1\n%2").arg(elapsedString).arg(name); bar->setLabelText(title); bar->setValue(++processed); } QApplication::processEvents(); ride = RideFileFactory::instance().openRideFile(context, file, errors); if (ride->isDataPresent(RideFile::lat)) { out << " with GPS datas " << "\r\n"; searchRouteInRide(ride, true, &out); } else out << " no GPS datas " << "\r\n"; if (bar && bar->wasCanceled()) { out << "SEARCH NEW ROUTE CANCELED: " << QDateTime::currentDateTime().toString() + "\r\n"; break; } } // now zap the progress bar if (bar) delete bar; // stop logging out << "SEARCH NEW ROUTE ENDS: " << QDateTime::currentDateTime().toString() + "\r\n"; QMessageBox::information(context->mainWindow, tr("Route"), tr("This route '%1' was found %2 times in %3 rides.").arg(this->getName()).arg(this->getRides().count()).arg(processed)); log.close(); } // This function converts decimal degrees to radians double deg2rad(double deg) { return (deg * pi / 180); } // This function converts radians to decimal degrees double rad2deg(double rad) { return (rad * 180 / pi); } // lat1, lon1 = point 1, Latitude and Longitude of // lat2, lon2 = Latitude and Longitude of point 2 double RouteSegment::distance(double lat1, double lon1, double lat2, double lon2) { double _theta, _dist; _theta = lon1 - lon2; if (_theta == 0 && (lat1 - lat2) == 0) _dist = 0; else { _dist = sin(deg2rad(lat1)) * sin(deg2rad(lat2)) + cos(deg2rad(lat1)) * cos(deg2rad(lat2)) * cos(deg2rad(_theta)); _dist = acos(_dist) * 6371; } return (_dist); } /* * Routes (list of RouteSegment) * * */ Routes::Routes(Context *context, const QDir &home) { this->home = home; this->context = context; connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(addRide(RideItem*))); connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(deleteRide(RideItem*))); readRoutes(); } void Routes::readRoutes() { QFile routeFile(home.canonicalPath() + "/routes.xml"); QXmlInputSource source( &routeFile ); QXmlSimpleReader xmlReader; RouteParser( handler ); xmlReader.setContentHandler(&handler); xmlReader.setErrorHandler(&handler); xmlReader.parse( source ); routes = handler.getRoutes(); } int Routes::newRoute(QString name) { RouteSegment add; add.setName(name); routes.insert(0, add); return 0; // always add at the top } void Routes::updateRoute(int index, QString name) { routes[index].setName(name); // save changes away writeRoutes(); } void Routes::deleteRoute(int index) { // now delete! routes.removeAt(index); writeRoutes(); } void Routes::writeRoutes() { // update routes.xml QString file = QString(home.canonicalPath() + "/routes.xml"); RouteParser::serialize(file, routes); routesChanged(); // signal! } // add a ride (after import / download) void Routes::addRide(RideItem* ride) { searchRoutesInRide(ride->ride()); writeRoutes(); } // delete a ride void Routes::deleteRide(RideItem* ride) { removeRideInRoutes(ride->ride()); } void Routes::removeRideInRoutes(RideFile* ride) { for (int routecount=0;routecountremoveRideInRoute(ride); } } void Routes::searchRoutesInRide(RideFile* ride) { // log of progress QFile log(context->athlete->home->logs().canonicalPath() + "/" + "routes2.log"); log.open(QIODevice::ReadWrite); QTextStream out(&log); out << "SEARCH ROUTES IN NEW FILE STARTS: " << QDateTime::currentDateTime().toString() << "\r\n"; for (int routecount=0;routecountgetName() << "(" << route->id().toString() << ")" << (routecount+1) << "/" << routes.count() << "\r\n"; route->searchRouteInRide(ride, false, &out); } // stop logging out << "SEARCH ROUTES IN NEW FILE ENDS: " << QDateTime::currentDateTime().toString() << "\r\n"; log.close(); } void Routes::createRouteFromInterval(IntervalItem *activeInterval) { // create a new route int index = context->athlete->routes->newRoute("route"); RouteSegment *route = &context->athlete->routes->routes[index]; QRegExp watts("\\([0-9]* *watts\\)"); QString name = activeInterval->text(0).trimmed(); if (name.contains(watts)) name = name.left(name.indexOf(watts)).trimmed(); if (name.length()<4 || name.startsWith("Selection #") ) name = QString(tr("Route #%1")).arg(context->athlete->routes->routes.length()); route->setName(name); // Construct the route with interval gps data double dist = 0, lastLat = 0, lastLon = 0; foreach (RideFilePoint *point, activeInterval->ride->dataPoints()) { if (point->secs >= activeInterval->start && point->secs < activeInterval->stop) { if (lastLat != 0 && lastLon != 0 && point->lat != 0 && point->lon != 0 && ceil(point->lat) != 180 && ceil(point->lon) != 180) { // distance ith last point double _dist = route->distance(lastLat, lastLon, point->lat, point->lon); if (_dist>=0.001) dist += _dist; if (dist>0.02) { RoutePoint _point = RoutePoint(point->lon, point->lat); route->addPoint(_point); dist = 0; } } lastLat = point->lat; lastLon = point->lon; } } // Search this route in all rides route->searchRouteInAllRides(context); // Save routes writeRoutes(); }