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