1 /**********************************************************************************************
2     Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
3 
4     This program is free software: you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation, either version 3 of the License, or
7     (at your option) any later version.
8 
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13 
14     You should have received a copy of the GNU General Public License
15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 
17 **********************************************************************************************/
18 
19 #include "canvas/CCanvas.h"
20 #include "device/CDeviceGarmin.h"
21 #include "device/CDeviceGarminArchive.h"
22 #include "gis/CGisListWks.h"
23 #include "gis/fit/CFitProject.h"
24 #include "gis/gpx/CGpxProject.h"
25 #include "gis/tcx/CTcxProject.h"
26 #include "gis/wpt/CGisItemWpt.h"
27 
28 #include <QtWidgets>
29 #include <QtXml>
30 
CDeviceGarmin(const QString & path,const QString & key,const QString & model,const QString & garminDeviceXml,QTreeWidget * parent)31 CDeviceGarmin::CDeviceGarmin(const QString& path, const QString& key, const QString& model, const QString& garminDeviceXml, QTreeWidget* parent)
32     : IDevice(path, eTypeGarmin, key, parent)
33     , cntImages(0)
34 {
35     setText(CGisListWks::eColumnName, "Garmin");
36 
37 
38     QFile file(QDir(path).absoluteFilePath(garminDeviceXml));
39     if(!file.open(QIODevice::ReadOnly))
40     {
41         qDebug() << "failed to open" << dir.absoluteFilePath(garminDeviceXml);
42     }
43 
44     QDomDocument dom;
45     QString msg;
46     int line;
47     int column;
48     if(!dom.setContent(&file, false, &msg, &line, &column))
49     {
50         qDebug() << QString("Failed to read: %1\nline %2, column %3:\n %4").arg(file.fileName()).arg(line).arg(column).arg(msg);
51     }
52 
53     file.close();
54 
55     const QDomElement& xmlDevice = dom.documentElement();
56     const QDomNode& xmlModel = xmlDevice.namedItem("Model");
57 
58     id = xmlDevice.namedItem("Id").toElement().text().trimmed();
59     description = xmlModel.namedItem("Description").toElement().text().trimmed();
60     partno = xmlModel.namedItem("PartNumber").toElement().text().trimmed();
61 
62     setText(CGisListWks::eColumnName, QString("%1 (%2)").arg(description, model));
63     setToolTip(CGisListWks::eColumnName, QString("%1 (%2, %3)").arg(description, partno, model));
64 
65     const QDomNode& xmlMassStorageMode = xmlDevice.namedItem("MassStorageMode");
66     const QDomNodeList& xmlDataTypes = xmlMassStorageMode.toElement().elementsByTagName("DataType");
67 
68     const int N = xmlDataTypes.count();
69     for(int n = 0; n < N; n++)
70     {
71         const QDomNode& xmlDataType = xmlDataTypes.item(n);
72         const QDomNode& xmlName = xmlDataType.namedItem("Name");
73         const QDomNode& xmlFile = xmlDataType.namedItem("File");
74         const QDomNode& xmlLocation = xmlFile.namedItem("Location");
75         const QDomNode& xmlPath = xmlLocation.namedItem("Path");
76 
77         QString name = xmlName.toElement().text().trimmed();
78 
79         if(name == "GPSData")
80         {
81             pathGpx = xmlPath.toElement().text().trimmed();
82         }
83         else if(name == "GeotaggedPhotos")
84         {
85             pathPictures = xmlPath.toElement().text().trimmed();
86         }
87         else if(name == "GeocachePhotos")
88         {
89             pathSpoilers = xmlPath.toElement().text().trimmed();
90         }
91         else if(name == "FIT_TYPE_4")
92         {
93             pathActivities = xmlPath.toElement().text().trimmed();
94         }
95         else if(name == "FIT_TYPE_6")
96         {
97             // courses
98             pathCourses = xmlPath.toElement().text().trimmed();
99         }
100         else if(name == "FIT_TYPE_8")
101         {
102             pathLocations = xmlPath.toElement().text().trimmed();
103         }
104         else if(name == "Adventures")
105         {
106             pathAdventures = xmlPath.toElement().text().trimmed();
107         }
108         else if(name == "FitnessCourses")
109         {
110             pathTcx = xmlPath.toElement().text().trimmed();
111         }
112     }
113 
114     qDebug() << dir.absoluteFilePath(pathGpx);
115     qDebug() << dir.absoluteFilePath(pathPictures);
116     qDebug() << dir.absoluteFilePath(pathSpoilers);
117     qDebug() << dir.absoluteFilePath(pathActivities);
118     qDebug() << dir.absoluteFilePath(pathCourses);
119     qDebug() << dir.absoluteFilePath(pathLocations);
120     qDebug() << dir.absoluteFilePath(pathAdventures);
121     qDebug() << dir.absoluteFilePath(pathTcx);
122 
123 
124     if(!dir.exists(pathGpx))
125     {
126         dir.mkpath(pathGpx);
127     }
128     if(!dir.exists(pathPictures))
129     {
130         dir.mkpath(pathPictures);
131     }
132     if(!dir.exists(pathSpoilers))
133     {
134         dir.mkpath(pathSpoilers);
135     }
136     if(!pathAdventures.isEmpty() && !dir.exists(pathAdventures))
137     {
138         dir.mkpath(pathAdventures);
139     }
140     if(!pathTcx.isEmpty() && !dir.exists(pathTcx))
141     {
142         dir.mkpath(pathTcx);
143     }
144 
145     createProjectsFromFiles(pathGpx, "gpx");
146     createProjectsFromFiles(pathGpx + "/Current", "gpx");
147 
148     QDir dirArchive(dir.absoluteFilePath(pathGpx + "/Archive"));
149     if(dirArchive.exists() && (dirArchive.entryList(QStringList("*.gpx")).count() != 0))
150     {
151         archive = new CDeviceGarminArchive(dir.absoluteFilePath(pathGpx + "/Archive"), this);
152     }
153 
154     createProjectsFromFiles(pathActivities, "fit");
155     createProjectsFromFiles(pathCourses, "fit");
156     createProjectsFromFiles(pathLocations, "fit");
157     if(!pathTcx.isEmpty())
158     {
159         createProjectsFromFiles(pathTcx, "tcx");
160     }
161 }
162 
createProjectsFromFiles(QString subdirecoty,QString fileEnding)163 void CDeviceGarmin::createProjectsFromFiles(QString subdirecoty, QString fileEnding)
164 {
165     QDir dirLoop(dir.absoluteFilePath(subdirecoty));
166     qDebug() << "reading files from device: " << dirLoop.path();
167     const QStringList& entries = dirLoop.entryList(QStringList("*." + fileEnding));
168     for(const QString& entry : entries)
169     {
170         const QString filename = dirLoop.absoluteFilePath(entry);
171         IGisProject* project = nullptr;
172         if(fileEnding == "fit")
173         {
174             project = new CFitProject(filename, this);
175         }
176         else if(fileEnding == "gpx")
177         {
178             project = new CGpxProject(filename, this);
179         }
180         else if(fileEnding == "tcx")
181         {
182             project = new CTcxProject(filename, this);
183         }
184 
185         if(project && !project->isValid())
186         {
187             delete project;
188         }
189     }
190 }
191 
~CDeviceGarmin()192 CDeviceGarmin::~CDeviceGarmin()
193 {
194 }
195 
insertCopyOfProject(IGisProject * project)196 void CDeviceGarmin::insertCopyOfProject(IGisProject* project)
197 {
198     if(description.startsWith("EDGE 5", Qt::CaseInsensitive))
199     {
200         insertCopyOfProjectAsTcx(project);
201     }
202     else
203     {
204         insertCopyOfProjectAsGpx(project);
205     }
206 }
207 
reorderProjects(IGisProject * project)208 void CDeviceGarmin::reorderProjects(IGisProject* project)
209 {
210     // move new project to top of any sub-folder/sub-device item
211     int newIdx = NOIDX;
212     const int myIdx = childCount() - 1;
213     for(int i = myIdx - 1; i >= 0; i--)
214     {
215         IDevice* device = dynamic_cast<IDevice*>(child(i));
216         if(0 == device)
217         {
218             break;
219         }
220 
221         newIdx = i;
222     }
223 
224     if(newIdx != NOIDX)
225     {
226         takeChild(myIdx);
227         insertChild(newIdx, project);
228     }
229 }
230 
simplifiedName(IGisProject * project)231 QString CDeviceGarmin::simplifiedName(IGisProject* project)
232 {
233     QString name = project->getName();
234     return name.remove(QRegExp("[^A-Za-z0-9_]"));
235 }
236 
createFileName(IGisProject * project,const QString & path,const QString & suffix)237 QString CDeviceGarmin::createFileName(IGisProject* project, const QString& path, const QString& suffix)
238 {
239     QDir dirTarget = dir.absoluteFilePath(path);
240     return dirTarget.absoluteFilePath(simplifiedName(project) + suffix);
241 }
242 
insertCopyOfProjectAsTcx(IGisProject * project)243 void CDeviceGarmin::insertCopyOfProjectAsTcx(IGisProject* project)
244 {
245     QString filename = createFileName(project, pathTcx, ".tcx");
246 
247     if(testForExternalProject(filename))
248     {
249         return;
250     }
251 
252     CTcxProject* tcx = new CTcxProject(filename, project, this);
253     if(!tcx->isValid())
254     {
255         delete tcx;
256         return;
257     }
258 
259 
260     if(!tcx->save())
261     {
262         delete tcx;
263         CCanvas::restoreOverrideCursor("~CSelectProjectDialog");
264         return;
265     }
266 
267 
268     // move new project to top of any sub-folder/sub-device item
269     reorderProjects(tcx);
270 }
271 
insertCopyOfProjectAsGpx(IGisProject * project)272 void CDeviceGarmin::insertCopyOfProjectAsGpx(IGisProject* project)
273 {
274     QString filename = createFileName(project, pathGpx, ".gpx");
275 
276     if(testForExternalProject(filename))
277     {
278         return;
279     }
280 
281     CGpxProject* gpx = new CGpxProject(filename, project, this);
282     if(!gpx->isValid())
283     {
284         delete gpx;
285         return;
286     }
287 
288     if(!gpx->save())
289     {
290         delete gpx;
291         return;
292     }
293 
294     createAdventureFromProject(project, pathGpx + "/" + simplifiedName(project) + ".gpx");
295 
296     // move new project to top of any sub-folder/sub-device item
297     reorderProjects(gpx);
298 }
299 
saveImages(CGisItemWpt & wpt)300 void CDeviceGarmin::saveImages(CGisItemWpt& wpt)
301 {
302     if(wpt.isGeocache())
303     {
304         QString name = wpt.getName();
305         quint32 size = name.size();
306         QString path = QString("%1/%2/%3").arg(name.at(size - 1)).arg(name.at(size - 2)).arg(name);
307 
308         const QDir dirSpoilers(dir.absoluteFilePath(pathSpoilers));
309         const QDir dirCache(dirSpoilers.absoluteFilePath(path));
310         const QList<CGisItemWpt::image_t>& images = wpt.getImages();
311 
312         if(!dirCache.exists())
313         {
314             dirCache.mkpath(dirCache.absolutePath());
315         }
316 
317         QString filename;
318         for(const CGisItemWpt::image_t& image : images)
319         {
320             filename = image.info;
321             filename = filename.remove(QRegExp("[^A-Za-z0-9_]"));
322 
323             if(!filename.endsWith("jpg"))
324             {
325                 filename += ".jpg";
326             }
327             image.pixmap.save(dirCache.absoluteFilePath(filename));
328         }
329     }
330     else
331     {
332         const QDir dirImages(dir.absoluteFilePath(pathPictures));
333         const QString& key = wpt.getKey().project;
334         const QList<CGisItemWpt::image_t>& images = wpt.getImages();
335         QList<IGisItem::link_t> links;
336 
337         QString filename;
338         for(const CGisItemWpt::image_t& image : images)
339         {
340             filename = QString("%1.%2.jpg").arg(key).arg(cntImages);
341             image.pixmap.save(dirImages.absoluteFilePath(filename));
342 
343             IGisItem::link_t link;
344             link.uri = pathPictures + "/" + filename;
345             link.text = tr("Picture%1").arg(cntImages);
346             link.type = "Garmin";
347 
348             links << link;
349 
350             cntImages++;
351         }
352 
353         wpt.appendLinks(links);
354     }
355 }
356 
loadImages(CGisItemWpt & wpt)357 void CDeviceGarmin::loadImages(CGisItemWpt& wpt)
358 {
359     if(wpt.isGeocache())
360     {
361         QString name = wpt.getName();
362         quint32 size = name.size();
363         QString path = QString("%1/%2/%3").arg(name.at(size - 1)).arg(name.at(size - 2)).arg(name);
364 
365         const QDir dirSpoilers(dir.absoluteFilePath(pathSpoilers));
366         const QDir dirCache(dirSpoilers.absoluteFilePath(path));
367 
368         QList<CGisItemWpt::image_t> images;
369         const QStringList& entries = dirCache.entryList(QStringList("*.jpg"), QDir::Files);
370         for(const QString& file : entries)
371         {
372             CGisItemWpt::image_t image;
373             image.pixmap.load(dirCache.absoluteFilePath(file));
374 
375             if(!image.pixmap.isNull())
376             {
377                 image.fileName = file;
378                 image.info = QFileInfo(file).completeBaseName();
379                 images << image;
380             }
381         }
382 
383         if(!images.isEmpty())
384         {
385             wpt.appendImages(images);
386         }
387     }
388     else
389     {
390         const QList<IGisItem::link_t>& links = wpt.getLinks();
391         QList<CGisItemWpt::image_t> images;
392 
393         for(const IGisItem::link_t& link : links)
394         {
395             if(link.type != "Garmin")
396             {
397                 continue;
398             }
399             CGisItemWpt::image_t image;
400             image.fileName = link.text;
401             image.pixmap.load(dir.absoluteFilePath(link.uri.toString()));
402 
403             images << image;
404         }
405 
406         if(!images.isEmpty())
407         {
408             wpt.appendImages(images);
409             wpt.removeLinksByType("Garmin");
410         }
411     }
412 }
413 
startSavingProject(IGisProject * project)414 void CDeviceGarmin::startSavingProject(IGisProject* project)
415 {
416     cntImages = 0;
417 }
418 
aboutToRemoveProject(IGisProject * project)419 void CDeviceGarmin::aboutToRemoveProject(IGisProject* project)
420 {
421     // remove images attached to project
422     const QString& key = project->getKey();
423     const QDir dirImages(dir.absoluteFilePath(pathPictures));
424     const QStringList& entries = dirImages.entryList(QStringList("*.jpg"), QDir::Files);
425     for(const QString& entry : entries)
426     {
427         QString filename = dirImages.absoluteFilePath(entry);
428         QFileInfo fi(filename);
429 
430         if(fi.baseName() == key)
431         {
432             QFile::remove(filename);
433         }
434     }
435 
436     // remove geo cache spoiler images
437     const int N = project->childCount();
438     for(int n = 0; n < N; n++)
439     {
440         CGisItemWpt* wpt = dynamic_cast<CGisItemWpt*>(project->child(n));
441         if(wpt && wpt->isGeocache())
442         {
443             QString name = wpt->getName();
444             quint32 size = name.size();
445             QString path = QString("%1/%2/%3").arg(name.at(size - 1)).arg(name.at(size - 2)).arg(name);
446 
447             QDir dirSpoilers(dir.absoluteFilePath(pathSpoilers));
448             QDir dirCache(dirSpoilers.absoluteFilePath(path));
449 
450             dirCache.removeRecursively();
451         }
452     }
453 
454     if(!pathAdventures.isEmpty())
455     {
456         const QDir dirAdventures(dir.absoluteFilePath(pathAdventures));
457         QString filename = dirAdventures.absoluteFilePath(key + ".adv");
458         QFile::remove(filename);
459     }
460 }
461 
462