1 /**********************************************************************************************
2     Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
3     Copyright (C) 2020 Henri Hornburg <hrnbg@t-online.de>
4 
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 
18 **********************************************************************************************/
19 
20 #include "canvas/CCanvas.h"
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/db/macros.h"
27 #include "gis/GeoMath.h"
28 #include "gis/IGisItem.h"
29 #include "gis/ovl/CGisItemOvlArea.h"
30 #include "gis/prj/IGisProject.h"
31 #include "gis/rte/CGisItemRte.h"
32 #include "gis/trk/CGisItemTrk.h"
33 #include "gis/wpt/CGisItemWpt.h"
34 #include "helpers/CDraw.h"
35 #include "helpers/CSettings.h"
36 #include "misc.h"
37 #include "units/IUnit.h"
38 
39 #include <QtSql>
40 #include <QtWidgets>
41 #include <QtXml>
42 
43 QMutex IGisItem::mutexItems(QMutex::Recursive);
44 
45 const QString IGisItem::noKey;
46 
47 const QString IGisItem::noName = IGisItem::tr("[no name]");
48 
49 QVector<IGisItem::color_t> IGisItem::colorMap;
50 
51 
IGisItem(IGisProject * parent,type_e typ,int idx)52 IGisItem::IGisItem(IGisProject* parent, type_e typ, int idx)
53     : QTreeWidgetItem(parent, typ)
54 {
55     int n = -1;
56     setFlags(QTreeWidgetItem::flags() & ~Qt::ItemIsDropEnabled);
57 
58     if(nullptr == parent)
59     {
60         return;
61     }
62 
63     key.project = parent->getKey();
64     key.device = parent->getDeviceKey();
65 
66     if(idx >= 0)
67     {
68         parent->removeChild(this);
69         parent->insertChild(idx, this);
70     }
71     else
72     {
73         if(type() == eTypeTrk)
74         {
75             for(n = parent->childCount() - 2; n >= 0; n--)
76             {
77                 /** @note   The order of item types to test is given by the order items read from
78                             the GPX file in the CGpxProject constructor.  */
79 
80                 int childType = parent->child(n)->type();
81                 if(childType == eTypeTrk)
82                 {
83                     parent->removeChild(this);
84                     parent->insertChild(n + 1, this);
85                     break;
86                 }
87             }
88         }
89         else if(type() == eTypeRte)
90         {
91             for(n = parent->childCount() - 2; n >= 0; n--)
92             {
93                 /** @note   The order of item types to test is given by the order items read from
94                             the GPX file in the CGpxProject constructor.  */
95 
96                 int childType = parent->child(n)->type();
97                 if( childType == eTypeRte || childType == eTypeTrk)
98                 {
99                     parent->removeChild(this);
100                     parent->insertChild(n + 1, this);
101                     break;
102                 }
103             }
104         }
105         else if(type() == eTypeWpt)
106         {
107             for(n = parent->childCount() - 2; n >= 0; n--)
108             {
109                 /** @note   The order of item types to test is given by the order items read from
110                             the GPX file in the CGpxProject constructor.  */
111 
112                 int childType = parent->child(n)->type();
113                 if(childType == eTypeWpt || childType == eTypeRte || childType == eTypeTrk)
114                 {
115                     parent->removeChild(this);
116                     parent->insertChild(n + 1, this);
117                     break;
118                 }
119             }
120         }
121         else if(type() == eTypeOvl)
122         {
123             for(n = parent->childCount() - 2; n >= 0; n--)
124             {
125                 /** @note   The order of item types to test is given by the order items read from
126                             the GPX file in the CGpxProject constructor.  */
127 
128                 int childType = parent->child(n)->type();
129                 if(childType == eTypeOvl || childType == eTypeWpt || childType == eTypeRte || childType == eTypeTrk)
130                 {
131                     parent->removeChild(this);
132                     parent->insertChild(n + 1, this);
133                     break;
134                 }
135             }
136         }
137 
138         if(n < 0)
139         {
140             parent->removeChild(this);
141             parent->insertChild(0, this);
142         }
143     }
144 }
145 
~IGisItem()146 IGisItem::~IGisItem()
147 {
148 }
149 
150 
init()151 void IGisItem::init()
152 {
153     colorMap =
154     {
155         {"Black", tr("Black"), QColor(Qt::black), QString("://icons/8x8/bullet_black.png"), QString("://icons/lines/line_black.png")}
156         , {"DarkRed", tr("Dark Red"), QColor(Qt::darkRed), QString("://icons/8x8/bullet_dark_red.png"), QString("://icons/lines/line_dark_red.png")}
157         , {"DarkGreen", tr("Dark Green"), QColor(Qt::darkGreen), QString("://icons/8x8/bullet_dark_green.png"), QString("://icons/lines/line_dark_green.png")}
158         , {"DarkYellow", tr("Dark Yellow"), QColor(Qt::darkYellow), QString("://icons/8x8/bullet_dark_yellow.png"), QString("://icons/lines/line_dark_yellow.png")}
159         , {"DarkBlue", tr("Dark Blue"), QColor(Qt::darkBlue), QString("://icons/8x8/bullet_dark_blue.png"), QString("://icons/lines/line_dark_blue.png")}
160         , {"DarkMagenta", tr("Dark Magenta"), QColor(Qt::darkMagenta), QString("://icons/8x8/bullet_dark_magenta.png"), QString("://icons/lines/line_dark_magenta.png")}
161         , {"DarkCyan", tr("Dark Cyan"), QColor(Qt::darkCyan), QString("://icons/8x8/bullet_dark_cyan.png"), QString("://icons/lines/line_dark_cyan.png")}
162         , {"LightGray", tr("Light Gray"), QColor(Qt::lightGray), QString("://icons/8x8/bullet_gray.png"), QString("://icons/lines/line_gray.png")}
163         , {"DarkGray", tr("Dark Gray"), QColor(Qt::darkGray), QString("://icons/8x8/bullet_dark_gray.png"), QString("://icons/lines/line_dark_gray.png")}
164         , {"Red", tr("Red"), QColor(Qt::red), QString("://icons/8x8/bullet_red.png"), QString("://icons/lines/line_red.png")}
165         , {"Green", tr("Green"), QColor(Qt::green), QString("://icons/8x8/bullet_green.png"), QString("://icons/lines/line_green.png")}
166         , {"Yellow", tr("Yellow"), QColor(Qt::yellow), QString("://icons/8x8/bullet_yellow.png"), QString("://icons/lines/line_yellow.png")}
167         , {"Blue", tr("Blue"), QColor(Qt::blue), QString("://icons/8x8/bullet_blue.png"), QString("://icons/lines/line_blue.png")}
168         , {"Magenta", tr("Magenta"), QColor(Qt::magenta), QString("://icons/8x8/bullet_magenta.png"), QString("://icons/lines/line_magenta.png")}
169         , {"Cyan", tr("Cyan"), QColor(Qt::cyan), QString("://icons/8x8/bullet_cyan.png"), QString("://icons/lines/line_cyan.png")}
170         , {"White", tr("White"), QColor(Qt::white), QString("://icons/8x8/bullet_white.png"), QString("://icons/lines/line_white.png")}
171         , {"Transparent", tr("Transparent"), QColor(Qt::transparent), QString(), QString("://icons/lines/line_transparent.png")}
172     };
173 }
174 
selectColor(QWidget * parent)175 qint32 IGisItem::selectColor(QWidget* parent)
176 {
177     qint32 colorIdx = NOIDX;
178     QMenu* menu = getColorMenu("", nullptr, "", parent);
179     QAction* action = menu->exec(QCursor::pos());
180 
181     if(action != nullptr)
182     {
183         bool ok = false;
184         colorIdx = action->property("colorIdx").toInt(&ok);
185         if(!ok)
186         {
187             colorIdx = NOIDX;
188         }
189     }
190 
191     delete menu;
192     return colorIdx;
193 }
194 
getColorMenu(const QString & title,QObject * obj,const char * slot,QWidget * parent)195 QMenu* IGisItem::getColorMenu(const QString& title, QObject* obj, const char* slot, QWidget* parent)
196 {
197     QMenu* menu = new QMenu(title, parent);
198     menu->setIcon(QIcon("://icons/32x32/SelectColor.png"));
199 
200     QAction* action;
201     for(qint32 i = 0; i < IGisItem::colorMap.size(); i++)
202     {
203         QPixmap pixmap(16, 16);
204         pixmap.fill(IGisItem::colorMap[i].color);
205         action = menu->addAction(QIcon(pixmap), IGisItem::colorMap[i].label);
206         action->setProperty("colorIdx", i);
207 
208         if(obj != nullptr)
209         {
210             QAction::connect(action, SIGNAL(triggered(bool)), obj, slot);
211         }
212     }
213     return menu;
214 }
215 
getParentProject() const216 IGisProject* IGisItem::getParentProject() const
217 {
218     return dynamic_cast<IGisProject*>(parent());
219 }
220 
genKey() const221 void IGisItem::genKey() const
222 {
223     if(key.item.isEmpty())
224     {
225         QByteArray buffer;
226         QDataStream stream(&buffer, QIODevice::WriteOnly);
227         stream.setByteOrder(QDataStream::LittleEndian);
228         stream.setVersion(QDataStream::Qt_5_2);
229 
230         *this >> stream;
231 
232         QCryptographicHash md5(QCryptographicHash::Md5);
233         md5.addData(buffer);
234         key.item = md5.result().toHex();
235     }
236     if(key.project.isEmpty())
237     {
238         IGisProject* project = getParentProject();
239         if(project)
240         {
241             key.project = project->getKey();
242         }
243     }
244 }
245 
loadFromDb(quint64 id,QSqlDatabase & db)246 void IGisItem::loadFromDb(quint64 id, QSqlDatabase& db)
247 {
248     QSqlQuery query(db);
249     query.prepare("SELECT data, keyqms, hash FROM items WHERE id=:id");
250     query.bindValue(":id", id);
251     QUERY_EXEC(return );
252     if(query.next())
253     {
254         QByteArray data(query.value(0).toByteArray());
255         QDataStream in(&data, QIODevice::ReadOnly);
256         in.setByteOrder(QDataStream::LittleEndian);
257         in.setVersion(QDataStream::Qt_5_2);
258         in >> history;
259         loadHistory(history.histIdxCurrent);
260 
261         if(key.item.isEmpty())
262         {
263             QString keyFromDB = query.value(1).toString();
264             /*[Issue #72] Database/Workspace inconsistency in QMS 1.4.0
265 
266                The root cause is a missing key in the serialized data. This is fixed by calling getKey() in setupHistory().
267 
268                As the database has a valid key the complete history data has to be fixed with that key.
269              */
270             const int N = history.events.size();
271             for(int i = 0; i < N; i++)
272             {
273                 loadHistory(i);
274                 key.item = keyFromDB;
275                 updateHistory();
276             }
277         }
278 
279         lastDatabaseHash = query.value(2).toString();
280     }
281 }
282 
updateFromDB(quint64 id,QSqlDatabase & db)283 void IGisItem::updateFromDB(quint64 id, QSqlDatabase& db)
284 {
285     QSqlQuery query(db);
286 
287     query.prepare("SELECT hash FROM items WHERE id=:id");
288     query.bindValue(":id", id);
289     QUERY_EXEC(return );
290 
291     /*
292         Test on the hash stored in the database. If the hash is
293         equal to the one stored in this item the item is up-to-date
294      */
295 
296     if(query.next())
297     {
298         if(query.value(0).toString() == lastDatabaseHash)
299         {
300             return;
301         }
302     }
303     else
304     {
305         // no hash? better leave...
306         return;
307     }
308 
309     // reset history and load item again
310     history.reset();
311     loadFromDb(id, db);
312 }
313 
getNameEx() const314 QString IGisItem::getNameEx() const
315 {
316     QString str = getName();
317     IGisProject* project = getParentProject();
318     if(project)
319     {
320         str += " @ " + project->getName();
321     }
322     IDevice* device = dynamic_cast<IDevice*>(parent()->parent());
323     if(device)
324     {
325         str += " @ " + device->getName();
326     }
327 
328     return str;
329 }
330 
331 
updateDecoration(quint32 enable,quint32 disable)332 void IGisItem::updateDecoration(quint32 enable, quint32 disable)
333 {
334     // update text and icon
335     setToolTip(CGisListWks::eColumnName, getInfo(IGisItem::eFeatureShowName));
336     setText(CGisListWks::eColumnName, getName());
337     setSymbol();
338 
339     // update project if necessary
340     IGisProject* project = getParentProject();
341     if(project && (enable & (eMarkChanged | eMarkNotPart | eMarkNotInDB)))
342     {
343         project->setChanged();
344     }
345 
346     // test for lost & found folder
347     if(project && project->getType() == IGisProject::eTypeLostFound)
348     {
349         setText(CGisListWks::eColumnDecoration, QString());
350         setToolTip(CGisListWks::eColumnDecoration, QString());
351         return;
352     }
353 
354     // set marks in column 1
355     quint32 mask = data(1, Qt::UserRole).toUInt();
356     mask |= enable;
357     mask &= ~disable;
358     setData(1, Qt::UserRole, mask);
359 
360     QString tt;
361     QString str;
362     if(mask & eMarkNotPart)
363     {
364         tt += tt.isEmpty() ? "" : "\n";
365         tt += tr("The item is not part of the project in the database.");
366         tt += tr("\nIt is either a new item or it has been deleted in the database by someone else.");
367         str += "?";
368     }
369     if(mask & eMarkNotInDB)
370     {
371         tt += tt.isEmpty() ? "" : "\n";
372         tt += tr("The item is not in the database.");
373         str += "X";
374     }
375     if(mask & eMarkChanged)
376     {
377         tt += tt.isEmpty() ? "" : "\n";
378         tt += tr("The item might need to be saved");
379         str += "*";
380     }
381     setText(CGisListWks::eColumnDecoration, str);
382     setToolTip(CGisListWks::eColumnDecoration, tt);
383 
384     //Set Rating column
385     if(!keywords.isEmpty())
386     {
387         QTreeWidgetItem::setIcon(CGisListWks::eColumnRating, QPixmap("://icons/32x32/Tag.png"));
388         setToolTip(CGisListWks::eColumnRating, QStringList(getKeywordsSorted()).join(", "));
389     }
390     else
391     {
392         QTreeWidgetItem::setIcon(CGisListWks::eColumnRating, QIcon());
393     }
394     if(rating > 0)
395     {
396         QTreeWidgetItem::setText(CGisListWks::eColumnRating, QString::number(rating));
397     }
398     else
399     {
400         QTreeWidgetItem::setText(CGisListWks::eColumnRating, "");
401     }
402 }
403 
404 
changed(const QString & what,const QString & icon)405 void IGisItem::changed(const QString& what, const QString& icon)
406 {
407     /*
408         If item gets changed but if it's origin is not QMapShack
409         then it is assumed to be tainted, as imported data should
410         never be changed without notice.
411      */
412     if(!(flags & eFlagCreatedInQms))
413     {
414         flags |= eFlagTainted;
415     }
416 
417     // forget all history entries after the current entry
418     for(int i = history.events.size() - 1; i > history.histIdxCurrent; i--)
419     {
420         history.events.pop_back();
421     }
422 
423     // append history by new entry
424     history.events << history_event_t();
425     history_event_t& event = history.events.last();
426     event.time = QDateTime::currentDateTimeUtc();
427     event.comment = what;
428     event.icon = icon;
429     event.who = CMainWindow::getUser();
430 
431     QDataStream stream(&event.data, QIODevice::WriteOnly);
432     stream.setByteOrder(QDataStream::LittleEndian);
433     stream.setVersion(QDataStream::Qt_5_2);
434 
435     *this >> stream;
436 
437     QCryptographicHash md5(QCryptographicHash::Md5);
438     md5.addData(event.data);
439     event.hash = md5.result().toHex();
440 
441     history.histIdxCurrent = history.events.size() - 1;
442 
443     updateDecoration(eMarkChanged, eMarkNone);
444 }
445 
updateHistory()446 void IGisItem::updateHistory()
447 {
448     if(history.histIdxCurrent == NOIDX)
449     {
450         return;
451     }
452 
453     history_event_t& event = history.events[history.histIdxCurrent];
454     event.data.clear();
455 
456     QDataStream stream(&event.data, QIODevice::WriteOnly);
457     stream.setByteOrder(QDataStream::LittleEndian);
458     stream.setVersion(QDataStream::Qt_5_2);
459 
460     *this >> stream;
461 
462     QCryptographicHash md5(QCryptographicHash::Md5);
463     md5.addData(event.data);
464     event.hash = md5.result().toHex();
465 
466     updateDecoration(eMarkChanged, eMarkNone);
467 }
468 
setupHistory()469 void IGisItem::setupHistory()
470 {
471     getKey();
472     history.histIdxInitial = NOIDX;
473     history.histIdxCurrent = NOIDX;
474 
475     // if history is empty setup an initial item
476     if(history.events.isEmpty())
477     {
478         history.events << history_event_t();
479         history_event_t& event = history.events.last();
480         event.time = QDateTime::currentDateTimeUtc();
481         event.comment = tr("Initial version.");
482         event.icon = "://icons/48x48/Start.png";
483     }
484 
485     // search for the first item with data
486     for(int i = 0; i < history.events.size(); i++)
487     {
488         if(!history.events[i].data.isEmpty())
489         {
490             history.histIdxInitial = i;
491             break;
492         }
493     }
494 
495     // if no initial item can be found fill the last item with data
496     // and make it the initial item
497     if(history.histIdxInitial == NOIDX)
498     {
499         history_event_t& event = history.events.last();
500 
501         QDataStream stream(&event.data, QIODevice::WriteOnly);
502         stream.setByteOrder(QDataStream::LittleEndian);
503         stream.setVersion(QDataStream::Qt_5_2);
504         *this >> stream;
505 
506         QCryptographicHash md5(QCryptographicHash::Md5);
507         md5.addData(event.data);
508         event.hash = md5.result().toHex();
509 
510         history.histIdxInitial = history.events.size() - 1;
511     }
512 
513     history.histIdxCurrent = history.events.size() - 1;
514 }
515 
loadHistory(int idx)516 void IGisItem::loadHistory(int idx)
517 {
518     // test for bad index
519     if((idx >= history.events.size()) || (idx < 0))
520     {
521         return;
522     }
523 
524     history_event_t& event = history.events[idx];
525 
526     // test for no data
527     if(event.data.isEmpty())
528     {
529         return;
530     }
531 
532     // restore item from history entry
533     QDataStream stream(&event.data, QIODevice::ReadOnly);
534     stream.setByteOrder(QDataStream::LittleEndian);
535     stream.setVersion(QDataStream::Qt_5_2);
536     *this << stream;
537 
538     history.histIdxCurrent = idx;
539 }
540 
cutHistoryAfter()541 void IGisItem::cutHistoryAfter()
542 {
543     while(history.events.size() > (history.histIdxCurrent + 1))
544     {
545         history.events.pop_back();
546     }
547 }
548 
cutHistoryBefore()549 void IGisItem::cutHistoryBefore()
550 {
551     for (int i = 0; i < history.histIdxCurrent; i++)
552     {
553         history.events[i].data.clear();
554     }
555 }
556 
squashHistory()557 void IGisItem::squashHistory()
558 {
559     if(history.events.size() < 2)
560     {
561         return;
562     }
563 
564     history_event_t& first = history.events.first();
565     history_event_t& last = history.events.last();
566 
567     last.time = first.time;
568     last.who = first.who;
569     last.icon = first.icon;
570     last.comment = first.comment;
571 
572     history.histIdxCurrent = 0;
573     history.histIdxInitial = 0;
574 
575     while(history.events.size() > 1)
576     {
577         history.events.pop_front();
578     }
579 }
580 
isReadOnly() const581 bool IGisItem::isReadOnly() const
582 {
583     return !(flags & eFlagWriteAllowed) || isOnDevice();
584 }
585 
isTainted() const586 bool IGisItem::isTainted() const
587 {
588     return flags & eFlagTainted;
589 }
590 
isOnDevice() const591 qint32 IGisItem::isOnDevice() const
592 {
593     IGisProject* project = getParentProject();
594     if(nullptr == project)
595     {
596         return false;
597     }
598     return project->isOnDevice();
599 }
600 
setReadOnlyMode(bool readOnly)601 bool IGisItem::setReadOnlyMode(bool readOnly)
602 {
603     // if the item is on a device no change is allowed
604     if(isOnDevice())
605     {
606         return false;
607     }
608 
609     // test if it is a change at all
610     if(isReadOnly() == readOnly)
611     {
612         return true;
613     }
614 
615     // warn if item is external and read only
616     if(!(flags & (eFlagCreatedInQms | eFlagTainted)))
617     {
618         SETTINGS;
619         bool doNotAsk = cfg.value("Dialog/Items/ReadOnly/doNotAsk", false).toBool();
620 
621         if(isReadOnly() && !readOnly && !doNotAsk)
622         {
623             CCanvasCursorLock cursorLock(Qt::ArrowCursor, __func__);
624 
625             QCheckBox* checkBox = new QCheckBox(tr("Never ask again."), 0);
626             QString msg = tr("<h3>%1</h3> This element is probably read-only because it was not created within QMapShack. Usually you should not want to change imported data. But if you think that is ok press 'Ok'.").arg(getName());
627             QMessageBox box(QMessageBox::Warning, tr("Read Only Mode..."), msg, QMessageBox::Ok | QMessageBox::Abort, CMainWindow::getBestWidgetForParent());
628             box.setDefaultButton(QMessageBox::Ok);
629             box.setCheckBox(checkBox);
630             int res = box.exec();
631 
632 
633             if(res != QMessageBox::Ok)
634             {
635                 return false;
636             }
637 
638             cfg.setValue("Dialog/Items/ReadOnly/doNotAsk", checkBox->isChecked());
639         }
640     }
641 
642     // finally change flag
643     if(readOnly)
644     {
645         flags &= ~eFlagWriteAllowed;
646     }
647     else
648     {
649         flags |= eFlagWriteAllowed;
650     }
651 
652     updateHistory();
653     return true;
654 }
655 
656 
getKey() const657 const IGisItem::key_t& IGisItem::getKey() const
658 {
659     if(key.item.isEmpty() || key.project.isEmpty())
660     {
661         genKey();
662     }
663     return key;
664 }
665 
getHash()666 const QString& IGisItem::getHash()
667 {
668     if(history.histIdxCurrent == NOIDX)
669     {
670         return noKey;
671     }
672     return history.events[history.histIdxCurrent].hash;
673 }
674 
675 
getLastDatabaseHash()676 const QString& IGisItem::getLastDatabaseHash()
677 {
678     if(lastDatabaseHash.isEmpty())
679     {
680         lastDatabaseHash = getHash();
681     }
682 
683     return lastDatabaseHash;
684 }
685 
setLastDatabaseHash(quint64 id,QSqlDatabase & db)686 void IGisItem::setLastDatabaseHash(quint64 id, QSqlDatabase& db)
687 {
688     lastDatabaseHash = getHash();
689 }
690 
setIcon(const QPixmap & icon)691 void IGisItem::setIcon(const QPixmap& icon)
692 {
693     this->icon = icon;
694     showIcon();
695 }
696 
showIcon()697 void IGisItem::showIcon()
698 {
699     if (isNogo())
700     {
701         const int& width = icon.width();
702         const int& height = icon.height();
703         displayIcon = QPixmap(width, height);
704         displayIcon.fill(Qt::transparent);
705         QPainter painter(&displayIcon);
706         painter.drawPixmap(0, 0, icon);
707         painter.drawPixmap(width * 0.4, height * 0.4, QPixmap("://icons/48x48/NoGo.png").scaled(width * 0.6, height * 0.6, Qt::KeepAspectRatio, Qt::SmoothTransformation));
708     }
709     else
710     {
711         displayIcon = icon;
712     }
713     QTreeWidgetItem::setIcon(CGisListWks::eColumnIcon, displayIcon);
714 }
715 
716 
str2color(const QString & name)717 QColor IGisItem::str2color(const QString& name)
718 {
719     for(int i = 0; i < colorMap.size(); i++)
720     {
721         if(QString(colorMap[i].name).toUpper() == name.toUpper())
722         {
723             return colorMap[i].color;
724         }
725     }
726 
727     return QColor(name);
728 }
729 
color2str(const QColor & color)730 QString IGisItem::color2str(const QColor& color)
731 {
732     for(int i = 0; i < colorMap.size(); i++)
733     {
734         if(colorMap[i].color == color)
735         {
736             return colorMap[i].name;
737         }
738     }
739 
740     return "";
741 }
742 
splitLineToViewport(const QPolygonF & line,const QRectF & extViewport,QList<QPolygonF> & lines)743 void IGisItem::splitLineToViewport(const QPolygonF& line, const QRectF& extViewport, QList<QPolygonF>& lines)
744 {
745     if(line.isEmpty())
746     {
747         return;
748     }
749 
750     QPointF ptt;
751     QPointF pt = line[0];
752 
753     QPolygonF subline;
754     subline << pt;
755 
756     const int size = line.size();
757     for(int i = 1; i < size; i++)
758     {
759         QPointF pt1 = line[i];
760 
761         if(!GPS_Math_LineCrossesRect(pt, pt1, extViewport))
762         {
763             pt = pt1;
764             if(subline.size() > 1)
765             {
766                 lines << subline;
767             }
768             subline.clear();
769             subline << pt;
770             continue;
771         }
772 
773         ptt = pt1 - pt;
774         if(ptt.manhattanLength() >= 5)
775         {
776             subline << pt1;
777             pt = pt1;
778         }
779     }
780 
781     if(subline.size() > 1)
782     {
783         lines << subline;
784     }
785 }
786 
removeHtml(const QString & str)787 QString IGisItem::removeHtml(const QString& str)
788 {
789     QTextDocument html;
790     html.setHtml(str);
791     return html.toPlainText();
792 }
793 
html2Dev(const QString & str,bool strictGpx11)794 QString IGisItem::html2Dev(const QString& str, bool strictGpx11)
795 {
796     // device or not, an empty text should never be enclosed in HTML tags
797     if(removeHtml(str).simplified().isEmpty())
798     {
799         return "";
800     }
801 
802     return (isOnDevice() == IDevice::eTypeGarmin) || strictGpx11 ? removeHtml(str) : str;
803 }
804 
toLink(bool isReadOnly,const QString & href,const QString & str,const QString & key)805 QString IGisItem::toLink(bool isReadOnly, const QString& href, const QString& str, const QString& key)
806 {
807     if(isReadOnly)
808     {
809         return QString("%1").arg(str);
810     }
811     if(key.isEmpty())
812     {
813         return QString("<a href='%1'>%2</a>").arg(href, str);
814     }
815     else
816     {
817         return QString("<a href='%1?key=%3'>%2</a>").arg(href, str, key);
818     }
819 }
820 
createText(bool isReadOnly,const QString & cmt,const QString & desc,const QList<link_t> & links,const QString & key)821 QString IGisItem::createText(bool isReadOnly, const QString& cmt, const QString& desc, const QList<link_t>& links, const QString& key)
822 {
823     QString str;
824     bool isEmpty;
825 
826     isEmpty = removeHtml(desc).simplified().isEmpty();
827     if(!isReadOnly || !isEmpty)
828     {
829         str += toLink(isReadOnly, "description", tr("<h4>Description:</h4>"), key);
830         if(!removeHtml(desc).simplified().isEmpty())
831         {
832             str += desc;
833         }
834     }
835 
836     isEmpty = removeHtml(cmt).simplified().isEmpty();
837     if(!isReadOnly || !isEmpty)
838     {
839         str += toLink(isReadOnly, "comment", tr("<h4>Comment:</h4>"), key);
840         if(!isEmpty)
841         {
842             str += cmt;
843         }
844     }
845 
846     isEmpty = links.isEmpty();
847     if(!isReadOnly || !isEmpty)
848     {
849         str += toLink(isReadOnly, "links", tr("<h4>Links:</h4>"), key);
850         if(!isEmpty)
851         {
852             for(const link_t& link : links)
853             {
854                 str += QString("<p><a href='%1'>%2</a></p>")
855                        .arg(
856                     link.uri.toString(),
857                     link.text.isEmpty() ? link.uri.toString() : link.text
858                     );
859             }
860         }
861     }
862     return str;
863 }
864 
createText(bool isReadOnly,const QString & desc,const QList<link_t> & links,const QString & key)865 QString IGisItem::createText(bool isReadOnly, const QString& desc, const QList<link_t>& links, const QString& key)
866 {
867     QString str;
868     bool isEmpty;
869 
870     isEmpty = removeHtml(desc).simplified().isEmpty();
871     if(!isReadOnly || !isEmpty)
872     {
873         str += toLink(isReadOnly, "description", tr("<h4>Description:</h4>"), key);
874         if(removeHtml(desc).simplified().isEmpty())
875         {
876             str += tr("<p>--- no description ---</p>");
877         }
878         else
879         {
880             str += desc;
881         }
882     }
883 
884     isEmpty = links.isEmpty();
885     if(!isReadOnly || !isEmpty)
886     {
887         str += toLink(isReadOnly, "links", tr("<h4>Links:</h4>"), key);
888         if(isEmpty)
889         {
890             str += tr("<p>--- no links ---</p>");
891         }
892         else
893         {
894             for(const link_t& link : links)
895             {
896                 str += QString("<p><a href='%1'>%2</a></p>")
897                        .arg(link.uri.toString(),
898                             link.text.isEmpty() ? link.uri.toString() : link.text
899                             );
900             }
901         }
902     }
903     return str;
904 }
905 
isVisible(const QRectF & rect,const QPolygonF & viewport,CGisDraw * gis)906 bool IGisItem::isVisible(const QRectF& rect, const QPolygonF& viewport, CGisDraw* gis)
907 {
908     QPolygonF tmp1;
909     tmp1 << rect.topLeft();
910     tmp1 << rect.topRight();
911     tmp1 << rect.bottomRight();
912     tmp1 << rect.bottomLeft();
913 
914     gis->convertRad2Px(tmp1);
915 
916     QPolygonF tmp2 = viewport;
917     gis->convertRad2Px(tmp2);
918 
919     return tmp2.boundingRect().intersects(tmp1.boundingRect());
920 }
921 
isVisible(const QPointF & point,const QPolygonF & viewport,CGisDraw * gis)922 bool IGisItem::isVisible(const QPointF& point, const QPolygonF& viewport, CGisDraw* gis)
923 {
924     QPolygonF tmp2 = viewport;
925     gis->convertRad2Px(tmp2);
926 
927     QPointF pt = point;
928     gis->convertRad2Px(pt);
929 
930     return tmp2.boundingRect().contains(pt);
931 }
932 
isChanged() const933 bool IGisItem::isChanged() const
934 {
935     return text(CGisListWks::eColumnDecoration).contains('*');
936 }
937 
isWithin(const QRectF & area,selflags_t flags,const QPolygonF & points)938 bool IGisItem::isWithin(const QRectF& area, selflags_t flags, const QPolygonF& points)
939 {
940     if(points.isEmpty())
941     {
942         return false;
943     }
944 
945     if(flags & eSelectionExact)
946     {
947         for(const QPointF& point : points)
948         {
949             if(!area.contains(point))
950             {
951                 return false;
952             }
953         }
954         return true;
955     }
956     else if(flags & eSelectionIntersect)
957     {
958         for(const QPointF& point : points)
959         {
960             if(area.contains(point))
961             {
962                 return true;
963             }
964         }
965         return false;
966     }
967 
968     return false;
969 }
970 
setNogo(bool yes)971 void IGisItem::setNogo(bool yes)
972 {
973     bool changed = false;
974     if(yes)
975     {
976         if(!isNogo())
977         {
978             setNogoFlag(true);
979             changed = true;
980         }
981     }
982     else
983     {
984         if(isNogo())
985         {
986             setNogoFlag(false);
987             changed = true;
988         }
989     }
990     if (changed)
991     {
992         showIcon();
993         updateHistory();
994     }
995 }
996 
setNogoFlag(bool yes)997 void IGisItem::setNogoFlag(bool yes)
998 {
999     if (yes)
1000     {
1001         flags |= eFlagNogo;
1002     }
1003     else
1004     {
1005         flags &= ~eFlagNogo;
1006     }
1007 }
1008 
getNogoTextureBrush()1009 const QBrush& IGisItem::getNogoTextureBrush()
1010 {
1011     static QBrush texture = []() -> QBrush {
1012                                 QPixmap texture(40, 40);
1013                                 QColor color = QColor(255, 0, 0, 77);
1014                                 texture.fill(color);
1015                                 QPainter painter(&texture);
1016                                 QPixmap nogo = QPixmap("://icons/48x48/NoGo.png").scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation);
1017                                 painter.setOpacity(0.5);
1018                                 painter.drawPixmap(0, 0, nogo);
1019                                 painter.drawPixmap(20, 20, nogo);
1020                                 return QBrush(texture);
1021                             } ();
1022     return texture;
1023 }
1024 
getNameAndProject(QString & name,IGisProject * & project,const QString & itemtype)1025 bool IGisItem::getNameAndProject(QString& name, IGisProject*& project, const QString& itemtype)
1026 {
1027     name = QInputDialog::getText(CMainWindow::getBestWidgetForParent(), tr("Edit name..."), tr("Enter new %1 name.").arg(itemtype), QLineEdit::Normal, name);
1028     if(name.isEmpty())
1029     {
1030         return false;
1031     }
1032 
1033     project = CGisWorkspace::self().selectProject(false);
1034     return nullptr != project;
1035 }
1036 
newGisItem(quint32 type,quint64 id,QSqlDatabase & db,IGisProject * project)1037 IGisItem* IGisItem::newGisItem(quint32 type, quint64 id, QSqlDatabase& db, IGisProject* project)
1038 {
1039     IGisItem* item = nullptr;
1040 
1041     // load item from database
1042     switch(type)
1043     {
1044     case IGisItem::eTypeWpt:
1045         item = new CGisItemWpt(id, db, project);
1046         break;
1047 
1048     case IGisItem::eTypeTrk:
1049         item = new CGisItemTrk(id, db, project);
1050         break;
1051 
1052     case IGisItem::eTypeRte:
1053         item = new CGisItemRte(id, db, project);
1054         break;
1055 
1056     case IGisItem::eTypeOvl:
1057         item = new CGisItemOvlArea(id, db, project);
1058         break;
1059 
1060     default:
1061         ;
1062     }
1063 
1064     return item;
1065 }
1066 
getRating() const1067 qreal IGisItem::getRating() const
1068 {
1069     return rating;
1070 }
1071 
setRating(qreal rating)1072 void IGisItem::setRating(qreal rating)
1073 {
1074     this->rating = rating;
1075     updateHistory();
1076 }
1077 
getKeywords() const1078 const QSet<QString>& IGisItem::getKeywords() const
1079 {
1080     return keywords;
1081 }
1082 
getKeywordsSorted() const1083 QList<QString> IGisItem::getKeywordsSorted() const
1084 {
1085     QList<QString> sortedKeywords = keywords.toList();
1086     std::sort(sortedKeywords.begin(), sortedKeywords.end(), sortByString);
1087     return sortedKeywords;
1088 }
1089 
addKeywords(const QSet<QString> & otherKeywords)1090 void IGisItem::addKeywords(const QSet<QString>& otherKeywords)
1091 {
1092     keywords.unite(otherKeywords);
1093     updateHistory();
1094 }
1095 
removeKeywords(const QSet<QString> & otherKeywords)1096 void IGisItem::removeKeywords(const QSet<QString>& otherKeywords)
1097 {
1098     keywords.subtract(otherKeywords);
1099     updateHistory();
1100 }
1101 
getRatingKeywordInfo() const1102 const QString IGisItem::getRatingKeywordInfo() const
1103 {
1104     QString str = "";
1105     if(getRating() > 0)
1106     {
1107         str += "<br/>\n";
1108         str += tr("Rating: ") + QString::number(getRating());
1109     }
1110     if(!getKeywordsSorted().isEmpty())
1111     {
1112         str += "<br/>\n";
1113         str += tr("Keywords: ") + QStringList(getKeywordsSorted()).join(", ");
1114     }
1115     return str;
1116 }
1117