1 /**********************************************************************************************
2     Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
3     Copyright (C) 2017 Norbert Truchsess <norbert.truchsess@t-online.de>
4     Copyright (C) 2019 Henri Hornburg <hrnbg@t-online.de>
5 
6     This program is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 
19 **********************************************************************************************/
20 
21 #include "CMainWindow.h"
22 #include "device/IDevice.h"
23 #include "gis/CGisDraw.h"
24 #include "gis/CGisListWks.h"
25 #include "gis/CGisWorkspace.h"
26 #include "gis/fit/CFitProject.h"
27 #include "gis/gpx/CGpxProject.h"
28 #include "gis/IGisItem.h"
29 #include "gis/ovl/CGisItemOvlArea.h"
30 #include "gis/prj/CDetailsPrj.h"
31 #include "gis/prj/IGisProject.h"
32 #include "gis/qlb/CQlbProject.h"
33 #include "gis/qms/CQmsProject.h"
34 #include "gis/rte/CGisItemRte.h"
35 #include "gis/rte/router/IRouter.h"
36 #include "gis/slf/CSlfProject.h"
37 #include "gis/suunto/CLogProject.h"
38 #include "gis/suunto/CSmlProject.h"
39 #include "gis/tcx/CTcxProject.h"
40 #include "gis/tcx/CTcxProject.h"
41 #include "gis/trk/CGisItemTrk.h"
42 #include "gis/wpt/CGisItemWpt.h"
43 #include "helpers/CProgressDialog.h"
44 #include "helpers/CSelectCopyAction.h"
45 #include "helpers/CSettings.h"
46 #include "misc.h"
47 
48 #include <QtWidgets>
49 
50 
51 const QString IGisProject::filedialogAllSupported = "All Supported (*.gpx *.GPX *.tcx *.TCX *.sml *.log *.qms *.qlb *.slf *.fit)";
52 const QString IGisProject::filedialogFilterGPX = "GPS Exchange Format (*.gpx *.GPX)";
53 const QString IGisProject::filedialogFilterTCX = "TCX Garmin Proprietary (*.tcx *.TCX)";
54 const QString IGisProject::filedialogFilterSML = "Suunto XML format (*.sml)";
55 const QString IGisProject::filedialogFilterLOG = "Openambit XML format (*.log)";
56 const QString IGisProject::filedialogFilterQLB = "QLandkarte Binary (*.qlb)";
57 const QString IGisProject::filedialogFilterQMS = "QMapShack Binary (*.qms)";
58 const QString IGisProject::filedialogFilterSLF = "Sigma Log Format (*.slf)";
59 const QString IGisProject::filedialogFilterFIT = "Garmin FIT Format (*.fit)";
60 const QString IGisProject::filedialogSaveFilters = filedialogFilterGPX + ";; " + filedialogFilterQLB + ";; " + filedialogFilterQMS + ";; " + filedialogFilterTCX;
61 const QString IGisProject::filedialogLoadFilters = filedialogAllSupported + ";; " + filedialogFilterGPX + ";; " + filedialogFilterTCX + ";; " + filedialogFilterSML + ";; " + filedialogFilterLOG + ";; " + filedialogFilterQLB + ";; " + filedialogFilterQMS + ";; " + filedialogFilterSLF + ";; " + filedialogFilterFIT;
62 
63 QString IGisProject::keyUserFocus;
64 
IGisProject(type_e type,const QString & filename,CGisListWks * parent)65 IGisProject::IGisProject(type_e type, const QString& filename, CGisListWks* parent)
66     : QTreeWidgetItem(parent)
67     , type(type)
68     , filename(filename)
69 {
70     memset(cntItemsByType, 0, sizeof(cntItemsByType));
71     setCheckState(CGisListWks::eColumnCheckBox, Qt::Checked);
72 
73     if(parent)
74     {
75         // move project up the list until there a re only projects, no devices
76         int newIdx = NOIDX;
77         const int myIdx = parent->topLevelItemCount() - 1;
78         for(int i = myIdx - 1; i >= 0; i--)
79         {
80             IDevice* device = dynamic_cast<IDevice*>(parent->topLevelItem(i));
81             if(device != nullptr)
82             {
83                 newIdx = i;
84                 continue;
85             }
86             break;
87         }
88 
89         if(newIdx != NOIDX)
90         {
91             parent->takeTopLevelItem(myIdx);
92             parent->insertTopLevelItem(newIdx, this);
93         }
94     }
95 }
96 
IGisProject(type_e type,const QString & filename,IDevice * parent)97 IGisProject::IGisProject(type_e type, const QString& filename, IDevice* parent)
98     : QTreeWidgetItem(parent)
99     , type(type)
100     , filename(filename)
101 {
102     memset(cntItemsByType, 0, sizeof(cntItemsByType));
103     setCheckState(CGisListWks::eColumnCheckBox, Qt::Checked);
104     nameSuffix = parent->getName();
105 }
106 
~IGisProject()107 IGisProject::~IGisProject()
108 {
109     delete dlgDetails;
110     if(key == keyUserFocus)
111     {
112         keyUserFocus.clear();
113     }
114 }
115 
create(const QString filename,CGisListWks * parent)116 IGisProject* IGisProject::create(const QString filename, CGisListWks* parent)
117 {
118     IGisProject* item = nullptr;
119     QString suffix = QFileInfo(filename).suffix().toLower();
120     if(suffix == "gpx")
121     {
122         item = new CGpxProject(filename, parent);
123     }
124     else if(suffix == "qms")
125     {
126         item = new CQmsProject(filename, parent);
127     }
128     else if(suffix == "slf")
129     {
130         item = new CSlfProject(filename);
131 
132         // the CSlfProject does not - as the other C*Project - register itself in the list
133         // of currently opened projects. This is done manually here.
134         if(parent)
135         {
136             parent->addProject(item);
137         }
138     }
139     else if(suffix == "fit")
140     {
141         item = new CFitProject(filename, parent);
142     }
143     else if(suffix == "tcx")
144     {
145         item = new CTcxProject(filename, parent);
146     }
147     else if (suffix == "sml")
148     {
149         item = new CSmlProject(filename, parent);
150     }
151     else if (suffix == "log")
152     {
153         item = new CLogProject(filename, parent);
154     }
155     else if (suffix == "qlb")
156     {
157         item = new CQlbProject(filename, parent);
158     }
159 
160     if(item && !item->isValid())
161     {
162         delete item;
163         item = nullptr;
164     }
165 
166     return item;
167 }
168 
html2Dev(const QString & str)169 QString IGisProject::html2Dev(const QString& str)
170 {
171     return isOnDevice() == IDevice::eTypeGarmin ? IGisItem::removeHtml(str) : str;
172 }
173 
askBeforClose()174 bool IGisProject::askBeforClose()
175 {
176     int res = QMessageBox::Ok;
177     if(isChanged())
178     {
179         {
180             CCanvasCursorLock cursorLock(Qt::ArrowCursor, __func__);
181             res = QMessageBox::question(CMainWindow::getBestWidgetForParent(), tr("Save project?"), tr("<h3>%1</h3>The project was changed. Save before closing it?").arg(getName()), QMessageBox::Save | QMessageBox::No | QMessageBox::Abort, QMessageBox::No);
182         }
183 
184         if(res == QMessageBox::Save)
185         {
186             // some project cannot be saved
187             if(canSave())
188             {
189                 save();
190             }
191             else
192             {
193                 saveAs();
194             }
195         }
196     }
197 
198     return res == QMessageBox::Abort;
199 }
200 
201 
isVisible() const202 bool IGisProject::isVisible() const
203 {
204     return checkState(CGisListWks::eColumnCheckBox) == Qt::Checked;
205 }
206 
207 
genKey() const208 void IGisProject::genKey() const
209 {
210     if(key.isEmpty())
211     {
212         QByteArray buffer;
213         QDataStream stream(&buffer, QIODevice::WriteOnly);
214         stream.setByteOrder(QDataStream::LittleEndian);
215         stream.setVersion(QDataStream::Qt_5_2);
216 
217         *this >> stream;
218 
219         QCryptographicHash md5(QCryptographicHash::Md5);
220         md5.addData(buffer);
221         key = md5.result().toHex();
222     }
223 }
224 
getDeviceKey() const225 QString IGisProject::getDeviceKey() const
226 {
227     IDevice* device = dynamic_cast<IDevice*>(parent());
228     if(device)
229     {
230         return device->getKey();
231     }
232 
233     return "";
234 }
235 
getIcon() const236 QPixmap IGisProject::getIcon() const
237 {
238     return icon(CGisListWks::eColumnIcon).pixmap(22, 22);
239 }
240 
isOnDevice() const241 qint32 IGisProject::isOnDevice() const
242 {
243     IDevice* device = dynamic_cast<IDevice*>(parent());
244     return device != nullptr ? device->type() : IDevice::eTypeNone;
245 }
246 
isChanged() const247 bool IGisProject::isChanged() const
248 {
249     return text(CGisListWks::eColumnDecoration).contains("*");
250 }
251 
edit()252 void IGisProject::edit()
253 {
254     if(dlgDetails.isNull())
255     {
256         dlgDetails = new CDetailsPrj(*this, 0);
257         dlgDetails->setObjectName(getName());
258     }
259 
260     CMainWindow::self().addWidgetToTab(dlgDetails);
261 }
262 
setName(const QString & str)263 void IGisProject::setName(const QString& str)
264 {
265     metadata.name = str;
266     setText(CGisListWks::eColumnName, getNameEx());
267     setChanged();
268 }
269 
setKeywords(const QString & str)270 void IGisProject::setKeywords(const QString& str)
271 {
272     metadata.keywords = str;
273     setChanged();
274 }
275 
setDescription(const QString & str)276 void IGisProject::setDescription(const QString& str)
277 {
278     metadata.desc = str;
279     setChanged();
280 }
281 
setLinks(const QList<IGisItem::link_t> & links)282 void IGisProject::setLinks(const QList<IGisItem::link_t>& links)
283 {
284     metadata.links = links;
285     setChanged();
286 }
287 
setSortingRoadbook(sorting_roadbook_e s)288 void IGisProject::setSortingRoadbook(sorting_roadbook_e s)
289 {
290     changedRoadbookMode = (s != sortingRoadbook);
291     sortingRoadbook = s;
292     if(changedRoadbookMode)
293     {
294         setChanged();
295     }
296 }
297 
setSortingFolder(sorting_folder_e s)298 void IGisProject::setSortingFolder(sorting_folder_e s)
299 {
300     bool changed = (s != sortingFolder);
301     sortingFolder = s;
302     sortItems();
303 
304     if(changed)
305     {
306         setChanged();
307         if(dlgDetails != nullptr)
308         {
309             dlgDetails->updateData();
310         }
311     }
312 }
313 
setChanged()314 void IGisProject::setChanged()
315 {
316     if(autoSave)
317     {
318         if(!autoSavePending)
319         {
320             autoSavePending = true;
321             CGisWorkspace::self().postEventForWks(new CEvtA2WSave(getKey()));
322         }
323     }
324 
325     if(autoSyncToDev)
326     {
327         if(!autoSyncToDevPending)
328         {
329             autoSyncToDevPending = true;
330             CGisWorkspace::self().postEventForWks(new CEvtA2WSync(getKey()));
331         }
332     }
333     updateDecoration(false);
334     updateItems();
335 }
336 
setAutoSave(bool on)337 void IGisProject::setAutoSave(bool on)
338 {
339     // make sure project is saved one more time to remove autoSave flag in storage
340     if(!on && autoSave)
341     {
342         CGisWorkspace::self().postEventForWks(new CEvtA2WSave(getKey()));
343     }
344 
345     autoSave = on;
346     setChanged();
347 }
348 
setAutoSyncToDevice(bool yes)349 void IGisProject::setAutoSyncToDevice(bool yes)
350 {
351     autoSyncToDev = yes;
352     updateDecoration();
353 }
354 
switchOnCorrelation()355 void IGisProject::switchOnCorrelation()
356 {
357     noCorrelation = false;
358     hashTrkWpt[0].clear();
359     hashTrkWpt[1].clear();
360     updateItems();
361 }
362 
updateItems()363 void IGisProject::updateItems()
364 {
365     if(noUpdate)
366     {
367         return;
368     }
369 
370     sortItems();
371     updateItemCounters();
372 
373     if(noCorrelation)
374     {
375         return;
376     }
377 
378     if(!changedRoadbookMode)
379     {
380         if((hashTrkWpt[0] == hashTrkWpt[1]) || (getItemCountByType(IGisItem::eTypeTrk) == 0))
381         {
382             return;
383         }
384     }
385     changedRoadbookMode = false;
386 
387 
388     quint32 total = cntTrkPts * cntWpts;
389     quint32 current = 0;
390 
391     PROGRESS_SETUP(tr("%1: Correlate tracks and waypoints.").arg(getName()), 0, total, CMainWindow::getBestWidgetForParent());
392 
393     for(int i = 0; i < childCount(); i++)
394     {
395         CGisItemTrk* trk = dynamic_cast<CGisItemTrk*>(child(i));
396         if(trk)
397         {
398             trk->findWaypointsCloseBy(progress, current);
399             if(progress.wasCanceled())
400             {
401                 QString msg = tr("<h3>%1</h3>Did that take too long for you? Do you want to skip correlation of tracks and waypoints for this project in the future?").arg(getNameEx());
402                 int res = QMessageBox::question(&progress, tr("Canceled correlation..."), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
403                 noCorrelation = res == QMessageBox::Yes;
404                 break;
405             }
406         }
407     }
408 
409     if(dlgDetails != nullptr)
410     {
411         dlgDetails->updateData();
412     }
413 }
414 
save()415 bool IGisProject::save()
416 {
417     if(!canSave())
418     {
419         qWarning() << "This should never be called!";
420         return false;
421     }
422 
423     return saveAs(filename, getFileDialogFilter());
424 }
425 
saveAs(QString fn,QString filter)426 bool IGisProject::saveAs(QString fn, QString filter)
427 {
428     SETTINGS;
429 
430     if(fn.isEmpty())
431     {
432         QString path = cfg.value("Paths/lastGisPath", QDir::homePath()).toString();
433 
434         // guess the correct extension:
435         // by default use the extension provided by the current format,
436         // otherwise use gpx
437         QString ext = getFileExtension();
438         filter = getFileDialogFilter();
439         if(ext.isEmpty() || !canSave())
440         {
441             ext = "gpx";
442             filter = IGisProject::filedialogFilterGPX;
443         }
444         path += "/" + getName() + "." + ext;
445 
446 
447         fn = QFileDialog::getSaveFileName(CMainWindow::getBestWidgetForParent(), tr("Save \"%1\" to...").arg(getName()), path, filedialogSaveFilters, &filter);
448 
449         if(fn.isEmpty())
450         {
451             return false;
452         }
453     }
454 
455     bool res = false;
456     if(filter == getFileDialogFilter())
457     {
458         filename = fn;
459         setupName(QFileInfo(fn).completeBaseName());
460     }
461 
462     if(filter == filedialogFilterGPX)
463     {
464         res = CGpxProject::saveAs(fn, *this, false);
465     }
466     else if(filter == filedialogFilterQMS)
467     {
468         res = CQmsProject::saveAs(fn, *this);
469     }
470     else if (filter == filedialogFilterTCX)
471     {
472         res = CTcxProject::saveAs(fn, *this);
473     }
474     else
475     {
476         return false;
477     }
478 
479     if(res && filter == getFileDialogFilter())
480     {
481         markAsSaved();
482     }
483 
484     QString path = QFileInfo(fn).absolutePath();
485     cfg.setValue("Paths/lastGisPath", path);
486 
487     return res;
488 }
489 
saveAsStrictGpx11()490 bool IGisProject::saveAsStrictGpx11()
491 {
492     SETTINGS;
493 
494     QString fn;
495     QString path = cfg.value("Paths/lastGisPath", QDir::homePath()).toString();
496 
497     // guess the correct extension:
498     // by default use the extension provided by the current format,
499     // otherwise use gpx
500     QString ext = "gpx";
501     QString filter = IGisProject::filedialogFilterGPX;
502     path += "/" + getName() + "." + ext;
503 
504     fn = QFileDialog::getSaveFileName(CMainWindow::getBestWidgetForParent(), tr("Save \"%1\" to...").arg(getName()), path, "Strict GPX V 1.1 (*.gpx *.GPX)", &filter);
505 
506     if(fn.isEmpty())
507     {
508         return false;
509     }
510 
511     bool res = CGpxProject::saveAs(fn, *this, true);
512 
513     path = QFileInfo(fn).absolutePath();
514     cfg.setValue("Paths/lastGisPath", path);
515 
516     return res;
517 }
518 
setupName(const QString & defaultName)519 void IGisProject::setupName(const QString& defaultName)
520 {
521     if(metadata.name.isEmpty())
522     {
523         metadata.name = defaultName;
524     }
525     setText(CGisListWks::eColumnName, getName());
526 }
527 
markAsSaved()528 void IGisProject::markAsSaved()
529 {
530     updateDecoration(true);
531     for(int i = 0; i < childCount(); i++)
532     {
533         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
534         if(nullptr == item)
535         {
536             continue;
537         }
538         item->updateDecoration(IGisItem::eMarkNone, IGisItem::eMarkChanged);
539     }
540 }
541 
getName() const542 QString IGisProject::getName() const
543 {
544     return metadata.name;
545 }
546 
getNameEx() const547 QString IGisProject::getNameEx() const
548 {
549     if(nameSuffix.isEmpty())
550     {
551         return metadata.name;
552     }
553     else
554     {
555         return metadata.name + " @ " + nameSuffix;
556     }
557 }
558 
559 
getInfo() const560 QString IGisProject::getInfo() const
561 {
562     QString str = metadata.name.isEmpty() ? text(CGisListWks::eColumnName) : metadata.name;
563     str = "<div style='font-weight: bold;'>" + str + "</div>";
564 
565     if(metadata.time.isValid())
566     {
567         str += "<br/>\n";
568         str += IUnit::datetime2string(metadata.time, false);
569     }
570 
571 
572     QString desc = IGisItem::removeHtml(metadata.desc).simplified();
573     if(!desc.isEmpty())
574     {
575         str += "<br/>\n";
576 
577         if(desc.count() < 100)
578         {
579             str += desc;
580         }
581         else
582         {
583             str += desc.left(97) + "...";
584         }
585     }
586 
587     if(!filename.isEmpty())
588     {
589         str += tr("<br/>\nFilename: %1").arg(filename);
590     }
591 
592     if(cntItemsByType[IGisItem::eTypeWpt])
593     {
594         str += "<br/>\n" + tr("Waypoints: %1").arg(cntItemsByType[IGisItem::eTypeWpt]);
595     }
596     if(cntItemsByType[IGisItem::eTypeTrk])
597     {
598         str += "<br/>\n" + tr("Tracks: %1").arg(cntItemsByType[IGisItem::eTypeTrk]);
599     }
600     if(cntItemsByType[IGisItem::eTypeRte])
601     {
602         str += "<br/>\n" + tr("Routes: %1").arg(cntItemsByType[IGisItem::eTypeRte]);
603     }
604     if(cntItemsByType[IGisItem::eTypeOvl])
605     {
606         str += "<br/>\n" + tr("Areas: %1").arg(cntItemsByType[IGisItem::eTypeOvl]);
607     }
608 
609     return str;
610 }
611 
612 
getItemByKey(const IGisItem::key_t & key)613 IGisItem* IGisProject::getItemByKey(const IGisItem::key_t& key)
614 {
615     for(int i = 0; i < childCount(); i++)
616     {
617         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
618         if(nullptr == item)
619         {
620             continue;
621         }
622 
623         if(item->getKey() == key)
624         {
625             return item;
626         }
627     }
628     return nullptr;
629 }
630 
getItemsByKeys(const QList<IGisItem::key_t> & keys,QList<IGisItem * > & items)631 void IGisProject::getItemsByKeys(const QList<IGisItem::key_t>& keys, QList<IGisItem*>& items)
632 {
633     for(int i = 0; i < childCount(); i++)
634     {
635         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
636         if(nullptr == item)
637         {
638             continue;
639         }
640 
641         if(keys.contains(item->getKey()))
642         {
643             items << item;
644         }
645     }
646 }
647 
getItemsByPos(const QPointF & pos,QList<IGisItem * > & items)648 void IGisProject::getItemsByPos(const QPointF& pos, QList<IGisItem*>& items)
649 {
650     if(!isVisible())
651     {
652         return;
653     }
654 
655     for(int i = 0; i < childCount(); i++)
656     {
657         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
658         if(nullptr == item || item->isHidden())
659         {
660             continue;
661         }
662 
663         if(item->isCloseTo(pos))
664         {
665             items << item;
666         }
667     }
668 }
669 
getItemsByArea(const QRectF & area,IGisItem::selflags_t flags,QList<IGisItem * > & items)670 void IGisProject::getItemsByArea(const QRectF& area, IGisItem::selflags_t flags, QList<IGisItem*>& items)
671 {
672     if(!isVisible())
673     {
674         return;
675     }
676 
677     for(int i = 0; i < childCount(); i++)
678     {
679         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
680         if(nullptr == item || item->isHidden())
681         {
682             continue;
683         }
684 
685         if(item->isWithin(area, flags))
686         {
687             items << item;
688         }
689     }
690 }
691 
getNogoAreas(QList<IGisItem * > & nogos) const692 void IGisProject::getNogoAreas(QList<IGisItem*>& nogos) const
693 {
694     if(!isVisible())
695     {
696         return;
697     }
698 
699     for(int i = 0; i < childCount(); i++)
700     {
701         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
702         if (item != nullptr && !item->isHidden() && item->isNogo())
703         {
704             nogos << item;
705         }
706     }
707 }
708 
709 
mouseMove(const QPointF & pos)710 void IGisProject::mouseMove(const QPointF& pos)
711 {
712     if(!isVisible())
713     {
714         return;
715     }
716 
717     for(int i = 0; i < childCount(); i++)
718     {
719         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
720         if(nullptr == item || item->isHidden())
721         {
722             continue;
723         }
724 
725         item->mouseMove(pos);
726     }
727 }
728 
729 
delItemByKey(const IGisItem::key_t & key,QMessageBox::StandardButtons & last)730 bool IGisProject::delItemByKey(const IGisItem::key_t& key, QMessageBox::StandardButtons& last)
731 {
732     for(int i = childCount(); i > 0; i--)
733     {
734         IGisItem* item = dynamic_cast<IGisItem*>(child(i - 1));
735         if(nullptr == item )
736         {
737             continue;
738         }
739 
740         if(item->getKey() == key)
741         {
742             if(last != QMessageBox::YesToAll)
743             {
744                 QString msg = tr("Are you sure you want to delete '%1' from project '%2'?").arg(item->getName(), text(CGisListWks::eColumnName));
745                 last = QMessageBox::question(CMainWindow::getBestWidgetForParent(), tr("Delete..."), msg, QMessageBox::YesToAll | QMessageBox::Cancel | QMessageBox::Ok | QMessageBox::No, QMessageBox::Ok);
746                 if((last == QMessageBox::No) || (last == QMessageBox::Cancel))
747                 {
748                     // as each item in the project has to be unique, we can stop searching.
749                     return false;
750                 }
751             }
752             delete item;
753 
754             /*
755                 Database projects are a bit different. Deleting an item does not really
756                 mean the project is changed as the item is still stored in the database.
757              */
758             if(type != eTypeDb)
759             {
760                 setChanged();
761             }
762 
763             // as each item in the project has to be unique, we can stop searching.
764             return true;
765         }
766     }
767     return false;
768 }
769 
editItemByKey(const IGisItem::key_t & key)770 void IGisProject::editItemByKey(const IGisItem::key_t& key)
771 {
772     for(int i = childCount(); i > 0; i--)
773     {
774         IGisItem* item = dynamic_cast<IGisItem*>(child(i - 1));
775         if(nullptr == item)
776         {
777             continue;
778         }
779 
780         if(item->getKey() == key)
781         {
782             item->edit();
783         }
784     }
785 }
786 
787 
insertCopyOfItem(IGisItem * item,int off,CSelectCopyAction::result_e & lastResult)788 void IGisProject::insertCopyOfItem(IGisItem* item, int off, CSelectCopyAction::result_e& lastResult)
789 {
790     bool clone = false;
791     IGisItem::key_t key = item->getKey();
792     key.project = getKey();
793     key.device = getDeviceKey();
794 
795     IGisItem* item2 = getItemByKey(key);
796     if(item2 != nullptr)
797     {
798         CSelectCopyAction::result_e result = lastResult;
799         if(lastResult == CSelectCopyAction::eResultNone)
800         {
801             CSelectCopyAction dlg(item, item2, CMainWindow::getBestWidgetForParent());
802             dlg.exec();
803             result = dlg.getResult();
804             if(dlg.allOthersToo())
805             {
806                 lastResult = result;
807             }
808         }
809 
810         if(result == CSelectCopyAction::eResultSkip)
811         {
812             return;
813         }
814         if(result == CSelectCopyAction::eResultNone)
815         {
816             return;
817         }
818         if(result == CSelectCopyAction::eResultClone)
819         {
820             clone = true;
821         }
822         else
823         {
824             // replace item2 with item
825             if(item != item2)
826             {
827                 delete item2;
828             }
829             else
830             {
831                 // replacing an item with itself does not make sense
832                 return;
833             }
834         }
835     }
836 
837     switch(item->type())
838     {
839     case IGisItem::eTypeTrk:
840     {
841         CGisItemTrk* trk = dynamic_cast<CGisItemTrk*>(item);
842         if(trk != nullptr)
843         {
844             CGisItemTrk* newTrk = new CGisItemTrk(*trk, this, off, clone);
845             // if the track is on a device, remove hidden trackpoints
846             if(isOnDevice())
847             {
848                 newTrk->filterDelete();
849             }
850         }
851         break;
852     }
853 
854     case IGisItem::eTypeWpt:
855     {
856         CGisItemWpt* wpt = dynamic_cast<CGisItemWpt*>(item);
857         if(wpt != nullptr)
858         {
859             new CGisItemWpt(*wpt, this, off, clone);
860         }
861         break;
862     }
863 
864     case IGisItem::eTypeRte:
865     {
866         CGisItemRte* rte = dynamic_cast<CGisItemRte*>(item);
867         if(rte != nullptr)
868         {
869             new CGisItemRte(*rte, this, off, clone);
870         }
871         break;
872     }
873 
874     case IGisItem::eTypeOvl:
875     {
876         CGisItemOvlArea* area = dynamic_cast<CGisItemOvlArea*>(item);
877         if(area != nullptr)
878         {
879             new CGisItemOvlArea(*area, this, off, clone);
880         }
881         break;
882     }
883     }
884 }
885 
drawItem(QPainter & p,const QPolygonF & viewport,QList<QRectF> & blockedAreas,CGisDraw * gis)886 void IGisProject::drawItem(QPainter& p, const QPolygonF& viewport, QList<QRectF>& blockedAreas, CGisDraw* gis)
887 {
888     if(!isVisible())
889     {
890         return;
891     }
892 
893     for(int i = 0; i < childCount(); i++)
894     {
895         if(gis->needsRedraw())
896         {
897             break;
898         }
899 
900         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
901         if(nullptr == item || item->isHidden())
902         {
903             continue;
904         }
905 
906         item->drawItem(p, viewport, blockedAreas, gis);
907     }
908 }
909 
drawItem(QPainter & p,const QRectF & viewport,CGisDraw * gis)910 void IGisProject::drawItem(QPainter& p, const QRectF& viewport, CGisDraw* gis)
911 {
912     if(!isVisible())
913     {
914         return;
915     }
916 
917     for(int i = 0; i < childCount(); i++)
918     {
919         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
920         if(nullptr == item || item->isHidden())
921         {
922             continue;
923         }
924 
925 
926         item->drawItem(p, viewport, gis);
927     }
928 }
929 
drawLabel(QPainter & p,const QPolygonF & viewport,QList<QRectF> & blockedAreas,const QFontMetricsF & fm,CGisDraw * gis)930 void IGisProject::drawLabel(QPainter& p, const QPolygonF& viewport, QList<QRectF>& blockedAreas, const QFontMetricsF& fm, CGisDraw* gis)
931 {
932     if(!isVisible())
933     {
934         return;
935     }
936 
937     for(int i = 0; i < childCount(); i++)
938     {
939         if(gis->needsRedraw())
940         {
941             break;
942         }
943 
944         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
945         if(nullptr == item || item->isHidden())
946         {
947             continue;
948         }
949 
950         item->drawLabel(p, viewport, blockedAreas, fm, gis);
951     }
952 }
953 
mount()954 void IGisProject::mount()
955 {
956     if(!isOnDevice())
957     {
958         return;
959     }
960     IDevice* device = dynamic_cast<IDevice*>(parent());
961     if(device)
962     {
963         device->mount();
964     }
965 }
966 
umount()967 void IGisProject::umount()
968 {
969     if(!isOnDevice())
970     {
971         return;
972     }
973     IDevice* device = dynamic_cast<IDevice*>(parent());
974     if(device)
975     {
976         device->umount();
977     }
978 }
979 
remove()980 bool IGisProject::remove()
981 {
982     CProjectMountLock mountLock(*this);
983 
984     /*
985        Check if parent is a device and give it a chance to take care of data.
986 
987        e.g. Garmin devices remove images attached to the project.
988      */
989     IDevice* device = dynamic_cast<IDevice*>(parent());
990     if(device)
991     {
992         device->aboutToRemoveProject(this);
993     }
994 
995     QFileInfo fi(filename);
996     if(fi.isFile())
997     {
998         QFile::remove(filename);
999     }
1000     else if(fi.isDir())
1001     {
1002         QDir(filename).removeRecursively();
1003     }
1004 
1005     return true;
1006 }
1007 
updateItemCounters()1008 void IGisProject::updateItemCounters()
1009 {
1010     // count number of items by type
1011     memset(cntItemsByType, 0, sizeof(cntItemsByType));
1012     cntTrkPts = 0;
1013     cntWpts = 0;
1014     totalDistance = 0;
1015     totalAscent = 0;
1016     totalDescent = 0;
1017     totalElapsedSeconds = 0;
1018     totalElapsedSecondsMoving = 0;
1019 
1020     QByteArray buffer;
1021     QDataStream stream(&buffer, QIODevice::WriteOnly);
1022     stream.setByteOrder(QDataStream::LittleEndian);
1023     stream.setVersion(QDataStream::Qt_5_2);
1024 
1025     for(int i = 0; i < childCount(); i++)
1026     {
1027         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
1028         if(nullptr == item)
1029         {
1030             continue;
1031         }
1032 
1033         cntItemsByType[item->type()]++;
1034 
1035         CGisItemTrk* trk = dynamic_cast<CGisItemTrk*>(item);
1036         if(trk)
1037         {
1038             cntTrkPts += trk->getNumberOfVisiblePoints();
1039             totalDistance += trk->getTotalDistance();
1040             totalAscent += trk->getTotalAscent();
1041             totalDescent += trk->getTotalDescent();
1042             totalElapsedSeconds += trk->getTotalElapsedSeconds();
1043             totalElapsedSecondsMoving += trk->getTotalElapsedSecondsMoving();
1044             stream << trk->getHash();
1045         }
1046 
1047         CGisItemWpt* wpt = dynamic_cast<CGisItemWpt*>(item);
1048         if(wpt)
1049         {
1050             cntWpts++;
1051             stream << wpt->getHash();
1052         }
1053     }
1054     QCryptographicHash md5(QCryptographicHash::Md5);
1055     md5.addData(buffer);
1056 
1057     hashTrkWpt[1] = hashTrkWpt[0];
1058     hashTrkWpt[0] = md5.result().toHex();
1059 }
1060 
blockUpdateItems(bool yes)1061 void IGisProject::blockUpdateItems(bool yes)
1062 {
1063     noUpdate = yes;
1064     if(noUpdate == false)
1065     {
1066         updateItems();
1067     }
1068 }
1069 
updateDecoration()1070 void IGisProject::updateDecoration()
1071 {
1072     int N = childCount();
1073     bool saved = true;
1074 
1075     for(int i = 0; i < N; i++)
1076     {
1077         IGisItem* item = dynamic_cast<IGisItem*>(child(i));
1078         if(nullptr == item)
1079         {
1080             continue;
1081         }
1082         if(item->isChanged())
1083         {
1084             saved = false;
1085             break;
1086         }
1087     }
1088     updateDecoration(saved);
1089 }
1090 
updateDecoration(bool saved)1091 void IGisProject::updateDecoration(bool saved)
1092 {
1093     QString str = autoSave ? "A" : saved ? "" : "*";
1094     if(autoSyncToDev)
1095     {
1096         str += "S";
1097     }
1098     setText(CGisListWks::eColumnDecoration, str);
1099 }
1100 
sortItems()1101 void IGisProject::sortItems()
1102 {
1103     QList<IGisItem*> trks;
1104     QList<IGisItem*> rtes;
1105     QList<IGisItem*> wpts;
1106     QList<IGisItem*> ovls;
1107 
1108     QList<QTreeWidgetItem*> items = takeChildren();
1109     QList<QTreeWidgetItem*> others; //For example Search
1110     for(QTreeWidgetItem* item : qAsConst(items))
1111     {
1112         CGisItemTrk* trk = dynamic_cast<CGisItemTrk*>(item);
1113         if(trk != nullptr)
1114         {
1115             trks << trk;
1116             continue;
1117         }
1118 
1119         CGisItemRte* rte = dynamic_cast<CGisItemRte*>(item);
1120         if(rte != nullptr)
1121         {
1122             rtes << rte;
1123             continue;
1124         }
1125 
1126         CGisItemWpt* wpt = dynamic_cast<CGisItemWpt*>(item);
1127         if(wpt != nullptr)
1128         {
1129             wpts << wpt;
1130             continue;
1131         }
1132 
1133         CGisItemOvlArea* ovl = dynamic_cast<CGisItemOvlArea*>(item);
1134         if(ovl != nullptr)
1135         {
1136             ovls << ovl;
1137             continue;
1138         }
1139 
1140         others << item;
1141     }
1142 
1143     sortItems(trks);
1144     sortItems(rtes);
1145     sortItems(wpts);
1146     sortItems(ovls);
1147 
1148     items.clear();
1149     items << others;
1150     for(IGisItem* item : qAsConst(trks))
1151     {
1152         items << item;
1153     }
1154     for(IGisItem* item : qAsConst(rtes))
1155     {
1156         items << item;
1157     }
1158     for(IGisItem* item : qAsConst(wpts))
1159     {
1160         items << item;
1161     }
1162     for(IGisItem* item : qAsConst(ovls))
1163     {
1164         items << item;
1165     }
1166 
1167     addChildren(items);
1168     if(projectFilter != nullptr)
1169     {
1170         projectFilter->showLineEdit(&projectSearch);
1171     }
1172     applyFilters();
1173 }
1174 
1175 
sortByTime(IGisItem * item1,IGisItem * item2)1176 static bool sortByTime(IGisItem* item1, IGisItem* item2)
1177 {
1178     const QDateTime& t1 = item1->getTimestamp();
1179     const QDateTime& t2 = item2->getTimestamp();
1180 
1181     // avoid jumping items due to invalid timestamps
1182     if(!t1.isValid() || !t2.isValid())
1183     {
1184         return sortByName<IGisItem>(item1, item2);
1185     }
1186 
1187     return t1 < t2;
1188 }
1189 
sortItems(QList<IGisItem * > & items) const1190 void IGisProject::sortItems(QList<IGisItem*>& items) const
1191 {
1192     switch(sortingFolder)
1193     {
1194     case IGisProject::eSortFolderName:
1195         qSort(items.begin(), items.end(), &sortByName<IGisItem>);
1196         break;
1197 
1198     case IGisProject::eSortFolderTime:
1199         qSort(items.begin(), items.end(), &sortByTime);
1200         break;
1201 
1202     case IGisProject::eSortFolderRating:
1203         qSort(items.begin(), items.end(), [](IGisItem* a, IGisItem* b){return a->getRating() > b->getRating();});
1204         break;
1205     }
1206 }
1207 
setProjectFilter(const CSearch & search)1208 void IGisProject::setProjectFilter(const CSearch& search)
1209 {
1210     projectSearch = search;
1211     applyFilters();
1212 }
1213 
setWorkspaceFilter(const CSearch & search)1214 void IGisProject::setWorkspaceFilter(const CSearch& search)
1215 {
1216     workspaceSearch = search;
1217     applyFilters();
1218 }
1219 
applyFilters()1220 void IGisProject::applyFilters()
1221 {
1222     const int N = childCount();
1223 
1224     for(int n = 0; n < N; n++)
1225     {
1226         IGisItem* item = dynamic_cast<IGisItem*>(child(n));
1227         if(item == nullptr)
1228         {
1229             continue;
1230         }
1231 
1232         bool projectFilterResult = projectSearch.getSearchResult(item);
1233         bool workspaceFilterResult = workspaceSearch.getSearchResult(item);
1234 
1235         item->setHidden(!(projectFilterResult && workspaceFilterResult));//get search result returns wether the object matches
1236     }
1237 }
1238 
findPolylineCloseBy(const QPointF & pt1,const QPointF & pt2,qint32 & threshold,QPolygonF & polyline)1239 bool IGisProject::findPolylineCloseBy(const QPointF& pt1, const QPointF& pt2, qint32& threshold, QPolygonF& polyline)
1240 {
1241     const int N = childCount();
1242     for(int n = 0; n < N; n++)
1243     {
1244         CGisItemTrk* trk = dynamic_cast<CGisItemTrk*>(child(n));
1245         if(trk != nullptr)
1246         {
1247             trk->findPolylineCloseBy(pt1, pt2, threshold, polyline);
1248         }
1249     }
1250     return !polyline.isEmpty();
1251 }
1252 
gainUserFocus(bool yes)1253 void IGisProject::gainUserFocus(bool yes)
1254 {
1255     if(yes)
1256     {
1257         setIcon(CGisListWks::eColumnName, QIcon("://icons/32x32/Focus.png"));
1258         keyUserFocus = key;
1259     }
1260     else
1261     {
1262         setIcon(CGisListWks::eColumnName, QIcon());
1263         keyUserFocus.clear();
1264     }
1265 }
1266 
filterProject(bool filter)1267 CProjectFilterItem* IGisProject::filterProject(bool filter)
1268 {
1269     if(filter)
1270     {
1271         if(projectFilter == nullptr)
1272         {
1273             projectFilter = new CProjectFilterItem(this);
1274             insertChild(0, projectFilter);
1275         }
1276         //Set expanded anyways to show that search exists
1277         projectFilter->showLineEdit();
1278         setExpanded(true);
1279     }
1280     else
1281     {
1282         removeChild(projectFilter);
1283         delete projectFilter;
1284         projectFilter = nullptr;
1285         projectSearch = CSearch("");
1286     }
1287     sortItems();
1288     return projectFilter;
1289 }
1290