/**********************************************************************************************
    Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
    Copyright (C) 2020 Henri Hornburg <hrnbg@t-online.de>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#include "gis/IGisItem.h"

#include <QtSql>
#include <QtWidgets>
#include <QtXml>

#include "CMainWindow.h"
#include "canvas/CCanvas.h"
#include "device/IDevice.h"
#include "gis/CGisDraw.h"
#include "gis/CGisListWks.h"
#include "gis/CGisWorkspace.h"
#include "gis/GeoMath.h"
#include "gis/db/macros.h"
#include "gis/ovl/CGisItemOvlArea.h"
#include "gis/prj/IGisProject.h"
#include "gis/rte/CGisItemRte.h"
#include "gis/trk/CGisItemTrk.h"
#include "gis/wpt/CGisItemWpt.h"
#include "helpers/CSettings.h"
#include "misc.h"
#include "units/IUnit.h"

QRecursiveMutex IGisItem::mutexItems;

const QString IGisItem::noKey;

const QString IGisItem::noName = IGisItem::tr("[no name]");

QVector<IGisItem::color_t> IGisItem::colorMap;

IGisItem::IGisItem(IGisProject* parent, type_e typ, int idx) : IWksItem(parent, typ) {
  int n = -1;
  setFlags(QTreeWidgetItem::flags() & ~Qt::ItemIsDropEnabled);

  if (nullptr == parent) {
    return;
  }

  key.project = parent->getKey();
  key.device = parent->getDeviceKey();

  if (idx >= 0) {
    parent->removeChild(this);
    parent->insertChild(idx, this);
  } else {
    if (type() == eTypeTrk) {
      for (n = parent->childCount() - 2; n >= 0; n--) {
        /** @note   The order of item types to test is given by the order items read from
                    the GPX file in the CGpxProject constructor.  */

        int childType = parent->child(n)->type();
        if (childType == eTypeTrk) {
          parent->removeChild(this);
          parent->insertChild(n + 1, this);
          break;
        }
      }
    } else if (type() == eTypeRte) {
      for (n = parent->childCount() - 2; n >= 0; n--) {
        /** @note   The order of item types to test is given by the order items read from
                    the GPX file in the CGpxProject constructor.  */

        int childType = parent->child(n)->type();
        if (childType == eTypeRte || childType == eTypeTrk) {
          parent->removeChild(this);
          parent->insertChild(n + 1, this);
          break;
        }
      }
    } else if (type() == eTypeWpt) {
      for (n = parent->childCount() - 2; n >= 0; n--) {
        /** @note   The order of item types to test is given by the order items read from
                    the GPX file in the CGpxProject constructor.  */

        int childType = parent->child(n)->type();
        if (childType == eTypeWpt || childType == eTypeRte || childType == eTypeTrk) {
          parent->removeChild(this);
          parent->insertChild(n + 1, this);
          break;
        }
      }
    } else if (type() == eTypeOvl) {
      for (n = parent->childCount() - 2; n >= 0; n--) {
        /** @note   The order of item types to test is given by the order items read from
                    the GPX file in the CGpxProject constructor.  */

        int childType = parent->child(n)->type();
        if (childType == eTypeOvl || childType == eTypeWpt || childType == eTypeRte || childType == eTypeTrk) {
          parent->removeChild(this);
          parent->insertChild(n + 1, this);
          break;
        }
      }
    }

    if (n < 0) {
      parent->removeChild(this);
      parent->insertChild(0, this);
    }
  }
}

IGisItem::~IGisItem() {}

void IGisItem::init() {
  colorMap = {
      {"Black", tr("Black"), QColor(Qt::black), "://icons/lines/line_black.png"},
      {"DarkRed", tr("Dark Red"), QColor(Qt::darkRed), QString("://icons/lines/line_dark_red.png")},
      {"DarkGreen", tr("Dark Green"), QColor(Qt::darkGreen), QString("://icons/lines/line_dark_green.png")},
      {"DarkYellow", tr("Dark Yellow"), QColor(Qt::darkYellow), QString("://icons/lines/line_dark_yellow.png")},
      {"DarkBlue", tr("Dark Blue"), QColor(Qt::darkBlue), QString("://icons/lines/line_dark_blue.png")},
      {"DarkMagenta", tr("Dark Magenta"), QColor(Qt::darkMagenta), QString("://icons/lines/line_dark_magenta.png")},
      {"DarkCyan", tr("Dark Cyan"), QColor(Qt::darkCyan), QString("://icons/lines/line_dark_cyan.png")},
      {"LightGray", tr("Light Gray"), QColor(Qt::lightGray), QString("://icons/lines/line_gray.png")},
      {"DarkGray", tr("Dark Gray"), QColor(Qt::darkGray), QString("://icons/lines/line_dark_gray.png")},
      {"Red", tr("Red"), QColor(Qt::red), QString("://icons/lines/line_red.png")},
      {"Green", tr("Green"), QColor(Qt::green), QString("://icons/lines/line_green.png")},
      {"Yellow", tr("Yellow"), QColor(Qt::yellow), QString("://icons/lines/line_yellow.png")},
      {"Blue", tr("Blue"), QColor(Qt::blue), QString("://icons/lines/line_blue.png")},
      {"Magenta", tr("Magenta"), QColor(Qt::magenta), QString("://icons/lines/line_magenta.png")},
      {"Cyan", tr("Cyan"), QColor(Qt::cyan), QString("://icons/lines/line_cyan.png")},
      {"White", tr("White"), QColor(Qt::white), QString("://icons/lines/line_white.png")},
      {"Transparent", tr("Transparent"), QColor(Qt::transparent), QString("://icons/lines/line_transparent.png")}};
}

qint32 IGisItem::selectColor(QWidget* parent) {
  qint32 colorIdx = NOIDX;
  QMenu* menu = getColorMenu("", nullptr, "", parent);
  QAction* action = menu->exec(QCursor::pos());

  if (action != nullptr) {
    bool ok = false;
    colorIdx = action->property("colorIdx").toInt(&ok);
    if (!ok) {
      colorIdx = NOIDX;
    }
  }

  delete menu;
  return colorIdx;
}

QMenu* IGisItem::getColorMenu(const QString& title, QObject* obj, const char* slot, QWidget* parent) {
  QMenu* menu = new QMenu(title, parent);
  menu->setIcon(QIcon("://icons/SelectColor.svgt"));

  QAction* action;
  for (qint32 i = 0; i < IGisItem::colorMap.size(); i++) {
    QPixmap pixmap(16, 16);
    pixmap.fill(IGisItem::colorMap[i].color);
    action = menu->addAction(QIcon(pixmap), IGisItem::colorMap[i].label);
    action->setProperty("colorIdx", i);

    if (obj != nullptr) {
      QAction::connect(action, SIGNAL(triggered(bool)), obj, slot);
    }
  }
  return menu;
}

IGisProject* IGisItem::getParentProject() const { return dynamic_cast<IGisProject*>(parent()); }

void IGisItem::genKey() const {
  if (key.item.isEmpty()) {
    QByteArray buffer;
    QDataStream stream(&buffer, QIODevice::WriteOnly);
    stream.setByteOrder(QDataStream::LittleEndian);
    stream.setVersion(QDataStream::Qt_5_2);

    *this >> stream;

    QCryptographicHash md5(QCryptographicHash::Md5);
    md5.addData(buffer);
    key.item = md5.result().toHex();
  }
  if (key.project.isEmpty()) {
    IGisProject* project = getParentProject();
    if (project) {
      key.project = project->getKey();
    }
  }
}

void IGisItem::loadFromDb(quint64 id, QSqlDatabase& db) {
  QSqlQuery query(db);
  query.prepare("SELECT data, keyqms, hash FROM items WHERE id=:id");
  query.bindValue(":id", id);
  QUERY_EXEC(return);
  if (query.next()) {
    QByteArray data(query.value(0).toByteArray());
    QDataStream in(&data, QIODevice::ReadOnly);
    in.setByteOrder(QDataStream::LittleEndian);
    in.setVersion(QDataStream::Qt_5_2);
    in >> history;
    loadHistory(history.histIdxCurrent);

    if (key.item.isEmpty()) {
      QString keyFromDB = query.value(1).toString();
      /*[Issue #72] Database/Workspace inconsistency in QMS 1.4.0

         The root cause is a missing key in the serialized data. This is fixed by calling getKey() in setupHistory().

         As the database has a valid key the complete history data has to be fixed with that key.
       */
      const int N = history.events.size();
      for (int i = 0; i < N; i++) {
        loadHistory(i);
        key.item = keyFromDB;
        updateHistory();
      }
    }

    lastDatabaseHash = query.value(2).toString();
  }
}

void IGisItem::updateFromDB(quint64 id, QSqlDatabase& db) {
  QSqlQuery query(db);

  query.prepare("SELECT hash FROM items WHERE id=:id");
  query.bindValue(":id", id);
  QUERY_EXEC(return);

  /*
      Test on the hash stored in the database. If the hash is
      equal to the one stored in this item the item is up-to-date
   */

  if (query.next()) {
    if (query.value(0).toString() == lastDatabaseHash) {
      return;
    }
  } else {
    // no hash? better leave...
    return;
  }

  // reset history and load item again
  history.reset();
  loadFromDb(id, db);
}

QString IGisItem::getNameEx() const {
  QString str = getName();
  IGisProject* project = getParentProject();
  if (project) {
    str += " @ " + project->getName();
  }
  IDevice* device = dynamic_cast<IDevice*>(parent()->parent());
  if (device) {
    str += " @ " + device->getName();
  }

  return str;
}

void IGisItem::updateDecoration(quint32 enable, quint32 disable) {
  // update text and icon
  setSymbol();

  // update project if necessary
  IGisProject* project = getParentProject();
  if (project && (enable & (eMarkChanged | eMarkNotPart | eMarkNotInDB))) {
    project->setChanged();
  }

  // test for lost & found folder
  if (project && project->getType() == IGisProject::eTypeLostFound) {
    return;
  }

  // set marks in column 1
  IWksItem::updateDecoration(enable, disable);
}

void IGisItem::changed(const QString& what, const QString& icon) {
  /*
      If item gets changed but if it's origin is not QMapShack
      then it is assumed to be tainted, as imported data should
      never be changed without notice.
   */
  if (!(flags & eFlagCreatedInQms)) {
    flags |= eFlagTainted;
  }

  // forget all history entries after the current entry
  for (int i = history.events.size() - 1; i > history.histIdxCurrent; i--) {
    history.events.pop_back();
  }

  // append history by new entry
  history.events << history_event_t();
  history_event_t& event = history.events.last();
  event.time = QDateTime::currentDateTimeUtc();
  event.comment = what;
  event.icon = icon;
  event.who = CMainWindow::getUser();

  QDataStream stream(&event.data, QIODevice::WriteOnly);
  stream.setByteOrder(QDataStream::LittleEndian);
  stream.setVersion(QDataStream::Qt_5_2);

  *this >> stream;

  QCryptographicHash md5(QCryptographicHash::Md5);
  md5.addData(event.data);
  event.hash = md5.result().toHex();

  history.histIdxCurrent = history.events.size() - 1;

  updateDecoration(eMarkChanged, eMarkNone);
}

void IGisItem::updateHistory() {
  if (history.histIdxCurrent == NOIDX) {
    return;
  }

  history_event_t& event = history.events[history.histIdxCurrent];
  event.data.clear();

  QDataStream stream(&event.data, QIODevice::WriteOnly);
  stream.setByteOrder(QDataStream::LittleEndian);
  stream.setVersion(QDataStream::Qt_5_2);

  *this >> stream;

  QCryptographicHash md5(QCryptographicHash::Md5);
  md5.addData(event.data);
  event.hash = md5.result().toHex();

  updateDecoration(eMarkChanged, eMarkNone);
}

void IGisItem::setupHistory() {
  getKey();
  history.histIdxInitial = NOIDX;
  history.histIdxCurrent = NOIDX;

  // if history is empty setup an initial item
  if (history.events.isEmpty()) {
    history.events << history_event_t();
    history_event_t& event = history.events.last();
    event.time = QDateTime::currentDateTimeUtc();
    event.comment = tr("Initial version.");
    event.icon = "://icons/Start.svgt";
  }

  // search for the first item with data
  for (int i = 0; i < history.events.size(); i++) {
    if (!history.events[i].data.isEmpty()) {
      history.histIdxInitial = i;
      break;
    }
  }

  // if no initial item can be found fill the last item with data
  // and make it the initial item
  if (history.histIdxInitial == NOIDX) {
    history_event_t& event = history.events.last();

    QDataStream stream(&event.data, QIODevice::WriteOnly);
    stream.setByteOrder(QDataStream::LittleEndian);
    stream.setVersion(QDataStream::Qt_5_2);
    *this >> stream;

    QCryptographicHash md5(QCryptographicHash::Md5);
    md5.addData(event.data);
    event.hash = md5.result().toHex();

    history.histIdxInitial = history.events.size() - 1;
  }

  history.histIdxCurrent = history.events.size() - 1;
}

void IGisItem::loadHistory(int idx) {
  // test for bad index
  if ((idx >= history.events.size()) || (idx < 0)) {
    return;
  }

  history_event_t& event = history.events[idx];

  // test for no data
  if (event.data.isEmpty()) {
    return;
  }

  // restore item from history entry
  QDataStream stream(&event.data, QIODevice::ReadOnly);
  stream.setByteOrder(QDataStream::LittleEndian);
  stream.setVersion(QDataStream::Qt_5_2);
  *this << stream;

  history.histIdxCurrent = idx;
}

void IGisItem::cutHistoryAfter() {
  while (history.events.size() > (history.histIdxCurrent + 1)) {
    history.events.pop_back();
  }
}

void IGisItem::cutHistoryBefore() {
  for (int i = 0; i < history.histIdxCurrent; i++) {
    history.events[i].data.clear();
  }
}

void IGisItem::squashHistory() {
  if (history.events.size() < 2) {
    return;
  }

  history_event_t& first = history.events.first();
  history_event_t& last = history.events.last();

  last.time = first.time;
  last.who = first.who;
  last.icon = first.icon;
  last.comment = first.comment;

  history.histIdxCurrent = 0;
  history.histIdxInitial = 0;

  while (history.events.size() > 1) {
    history.events.pop_front();
  }
}

bool IGisItem::isReadOnly() const { return !(flags & eFlagWriteAllowed) || isOnDevice(); }

bool IGisItem::isTainted() const { return flags & eFlagTainted; }

const qint32 IGisItem::isOnDevice() const {
  IGisProject* project = getParentProject();
  if (nullptr == project) {
    return false;
  }
  return project->isOnDevice();
}

bool IGisItem::setReadOnlyMode(bool readOnly) {
  // if the item is on a device no change is allowed
  if (isOnDevice()) {
    return false;
  }

  // test if it is a change at all
  if (isReadOnly() == readOnly) {
    return true;
  }

  // warn if item is external and read only
  if (!(flags & (eFlagCreatedInQms | eFlagTainted))) {
    SETTINGS;
    bool doNotAsk = cfg.value("Dialog/Items/ReadOnly/doNotAsk", false).toBool();

    if (isReadOnly() && !readOnly && !doNotAsk) {
      CCanvasCursorLock cursorLock(Qt::ArrowCursor, __func__);

      QCheckBox* checkBox = new QCheckBox(tr("Never ask again."), 0);
      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());
      QMessageBox box(QMessageBox::Warning, tr("Read Only Mode..."), msg, QMessageBox::Ok | QMessageBox::Abort,
                      CMainWindow::getBestWidgetForParent());
      box.setDefaultButton(QMessageBox::Ok);
      box.setCheckBox(checkBox);
      int res = box.exec();

      if (res != QMessageBox::Ok) {
        return false;
      }

      cfg.setValue("Dialog/Items/ReadOnly/doNotAsk", checkBox->isChecked());
    }
  }

  // finally change flag
  if (readOnly) {
    flags &= ~eFlagWriteAllowed;
  } else {
    flags |= eFlagWriteAllowed;
  }

  updateHistory();
  return true;
}

const IGisItem::key_t& IGisItem::getKey() const {
  if (key.item.isEmpty() || key.project.isEmpty()) {
    genKey();
  }
  return key;
}

const QString& IGisItem::getHash() {
  if (history.histIdxCurrent == NOIDX) {
    return noKey;
  }
  return history.events[history.histIdxCurrent].hash;
}

const QString& IGisItem::getLastDatabaseHash() {
  if (lastDatabaseHash.isEmpty()) {
    lastDatabaseHash = getHash();
  }

  return lastDatabaseHash;
}

void IGisItem::setLastDatabaseHash(quint64 id, QSqlDatabase& db) { lastDatabaseHash = getHash(); }

void IGisItem::setIcon(const QPixmap& icon) {
  // Stash the raw pixmap in displayIcon; showIcon() composes the framed icon from it.
  displayIcon = icon;
  showIcon();
}

void IGisItem::showIcon() {
  // displayIcon holds the raw pixmap on entry -- composing from a previous composite would bake
  // the nogo badge in for good. The copy survives the displayIcon reassignment below (COW).
  const QPixmap base = displayIcon;
  const int width = base.width();
  const int height = base.height();
  // center icon within surrounding square
  int size = qMax(width, height);
  displayIcon = QPixmap(size, size);
  displayIcon.fill(Qt::transparent);
  QPainter painter(&displayIcon);
  int dw = (size - width) / 2;
  int dh = (size - height) / 2;
  painter.drawPixmap(dw, dh, base);
  if (isNogo()) {
    painter.drawPixmap(width * 0.4 + dw, height * 0.4 + dh,
                       QPixmap("://icons/48x48/NoGo.png")
                           .scaled(width * 0.6, height * 0.6, Qt::KeepAspectRatio, Qt::SmoothTransformation));
  }
  icon = QIcon(displayIcon);
}

QColor IGisItem::str2color(const QString& name) {
  for (int i = 0; i < colorMap.size(); i++) {
    if (QString(colorMap[i].name).toUpper() == name.toUpper()) {
      return colorMap[i].color;
    }
  }

  return QColor(name);
}

QString IGisItem::color2str(const QColor& color) {
  for (int i = 0; i < colorMap.size(); i++) {
    if (colorMap[i].color == color) {
      return colorMap[i].name;
    }
  }

  return "";
}

void IGisItem::splitLineToViewport(const QPolygonF& line, const QRectF& extViewport, QList<QPolygonF>& lines) {
  if (line.isEmpty()) {
    return;
  }

  QPointF ptt;
  QPointF pt = line[0];

  QPolygonF subline;
  subline << pt;

  const int size = line.size();
  for (int i = 1; i < size; i++) {
    QPointF pt1 = line[i];

    if (!GPS_Math_LineCrossesRect(pt, pt1, extViewport)) {
      pt = pt1;
      if (subline.size() > 1) {
        lines << subline;
      }
      subline.clear();
      subline << pt;
      continue;
    }

    ptt = pt1 - pt;
    if (ptt.manhattanLength() >= 5) {
      subline << pt1;
      pt = pt1;
    }
  }

  if (subline.size() > 1) {
    lines << subline;
  }
}

QString IGisItem::removeHtml(const QString& str) {
  QTextDocument html;
  html.setHtml(str);
  return html.toPlainText();
}

QString IGisItem::savedIconPath(const QString& path) {
  // The on-disk form of a history icon is the portable 48x48 PNG, permanently. It is always present
  // and readable by any build, unlike the themable ".svgt" live form which renders blank without the
  // icon engine. Map any icons/<name>.{png,svg,svgt} to the PNG at save time; an unknown path passes
  // through, so a stray ".svg"/".svgt" from an old build heals to PNG on the next save.
  static const QRegularExpression re("^:+/*icons/(?:48x48/)?([A-Za-z0-9_]+)\\.(?:png|svg|svgt)$");

  const QRegularExpressionMatch match = re.match(path);
  if (!match.hasMatch()) {
    return path;
  }

  const QString png = QString("://icons/48x48/%1.png").arg(match.captured(1));
  return QFile::exists(png) ? png : path;
}

QString IGisItem::displayIconPath(const QString& path) {
  // Resolve the portable PNG on-disk form to the themable ".svgt" live form. Applied on load, so
  // history events live as ".svgt" in memory and follow the theme; savedIconPath() converts back to
  // PNG on save. A PNG from an old file and a ".svgt" from this session both normalise to the ".svgt".
  static const QRegularExpression re("^:+/*icons/(?:48x48/)?([A-Za-z0-9_]+)\\.(?:png|svg|svgt)$");

  const QRegularExpressionMatch match = re.match(path);
  if (!match.hasMatch()) {
    return path;
  }

  const QString svgt = QString("://icons/%1.svgt").arg(match.captured(1));
  return QFile::exists(svgt) ? svgt : path;
}

QString IGisItem::html2Dev(const QString& str, bool strictGpx11) {
  // device or not, an empty text should never be enclosed in HTML tags
  if (removeHtml(str).simplified().isEmpty()) {
    return "";
  }

  return (isOnDevice() == IDevice::eTypeGarmin) || (isOnDevice() == IDevice::eTypeGarminMtp) ||
                 (isOnDevice() == IDevice::eTypeGenericMtp) || strictGpx11
             ? removeHtml(str)
             : str;
}

QString IGisItem::toLink(bool isReadOnly, const QString& href, const QString& str, const QString& key) {
  if (isReadOnly) {
    return QString("%1").arg(str);
  }
  if (key.isEmpty()) {
    return QString("<a href='%1'>%2</a>").arg(href, str);
  } else {
    return QString("<a href='%1?key=%3'>%2</a>").arg(href, str, key);
  }
}

QString IGisItem::createText(bool isReadOnly, const QString& cmt, const QString& desc, const QList<link_t>& links,
                             const QString& key) {
  QString str;
  bool isEmpty;

  isEmpty = removeHtml(desc).simplified().isEmpty();
  if (!isReadOnly || !isEmpty) {
    str += toLink(isReadOnly, "description", tr("<h4>Description:</h4>"), key);
    if (!removeHtml(desc).simplified().isEmpty()) {
      str += desc;
    }
  }

  isEmpty = removeHtml(cmt).simplified().isEmpty();
  if (!isReadOnly || !isEmpty) {
    str += toLink(isReadOnly, "comment", tr("<h4>Comment:</h4>"), key);
    if (!isEmpty) {
      str += cmt;
    }
  }

  isEmpty = links.isEmpty();
  if (!isReadOnly || !isEmpty) {
    str += toLink(isReadOnly, "links", tr("<h4>Links:</h4>"), key);
    if (!isEmpty) {
      for (const link_t& link : links) {
        str += QString("<p><a href='%1'>%2</a></p>")
                   .arg(link.uri.toString(), link.text.isEmpty() ? link.uri.toString() : link.text);
      }
    }
  }
  return str;
}

QString IGisItem::createText(bool isReadOnly, const QString& desc, const QList<link_t>& links, const QString& key) {
  QString str;
  bool isEmpty;

  isEmpty = removeHtml(desc).simplified().isEmpty();
  if (!isReadOnly || !isEmpty) {
    str += toLink(isReadOnly, "description", tr("<h4>Description:</h4>"), key);
    if (removeHtml(desc).simplified().isEmpty()) {
      str += tr("<p>--- no description ---</p>");
    } else {
      str += desc;
    }
  }

  isEmpty = links.isEmpty();
  if (!isReadOnly || !isEmpty) {
    str += toLink(isReadOnly, "links", tr("<h4>Links:</h4>"), key);
    if (isEmpty) {
      str += tr("<p>--- no links ---</p>");
    } else {
      for (const link_t& link : links) {
        str += QString("<p><a href='%1'>%2</a></p>")
                   .arg(link.uri.toString(), link.text.isEmpty() ? link.uri.toString() : link.text);
      }
    }
  }
  return str;
}

const bool IGisItem::isVisible() const {
  IGisProject* project = getParentProject();
  if (project == nullptr) {
    return false;
  }
  return project->isVisible();
}

bool IGisItem::isVisible(const QRectF& rect, const QPolygonF& viewport, CGisDraw* gis) {
  QPolygonF tmp1;
  tmp1 << rect.topLeft();
  tmp1 << rect.topRight();
  tmp1 << rect.bottomRight();
  tmp1 << rect.bottomLeft();

  gis->convertRad2Px(tmp1);

  QPolygonF tmp2 = viewport;
  gis->convertRad2Px(tmp2);

  return tmp2.boundingRect().intersects(tmp1.boundingRect());
}

bool IGisItem::isVisible(const QPointF& point, const QPolygonF& viewport, CGisDraw* gis) {
  QPolygonF tmp2 = viewport;
  gis->convertRad2Px(tmp2);

  QPointF pt = point;
  gis->convertRad2Px(pt);

  return tmp2.boundingRect().contains(pt);
}

bool IGisItem::isWithin(const QRectF& area, selflags_t flags, const QPolygonF& points) {
  if (points.isEmpty()) {
    return false;
  }

  if (flags & eSelectionExact) {
    for (const QPointF& point : points) {
      if (!area.contains(point)) {
        return false;
      }
    }
    return true;
  } else if (flags & eSelectionIntersect) {
    for (const QPointF& point : points) {
      if (area.contains(point)) {
        return true;
      }
    }
    return false;
  }

  return false;
}

void IGisItem::setNogo(bool yes) {
  bool changed = false;
  if (yes) {
    if (!isNogo()) {
      setNogoFlag(true);
      changed = true;
    }
  } else {
    if (isNogo()) {
      setNogoFlag(false);
      changed = true;
    }
  }
  if (changed) {
    updateHistory();  // updateDecoration() re-seeds the icon, badge included
  }
}

void IGisItem::setNogoFlag(bool yes) {
  if (yes) {
    flags |= eFlagNogo;
  } else {
    flags &= ~eFlagNogo;
  }
}

const QBrush& IGisItem::getNogoTextureBrush() {
  static QBrush texture = []() -> QBrush {
    QPixmap texture(40, 40);
    QColor color = QColor(255, 0, 0, 77);
    texture.fill(color);
    QPainter painter(&texture);
    QPixmap nogo = QPixmap("://icons/48x48/NoGo.png").scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation);
    painter.setOpacity(0.5);
    painter.drawPixmap(0, 0, nogo);
    painter.drawPixmap(20, 20, nogo);
    return QBrush(texture);
  }();
  return texture;
}

bool IGisItem::getNameAndProject(QString& name, IGisProject*& project, const QString& itemtype) {
  name = QInputDialog::getText(CMainWindow::getBestWidgetForParent(), tr("Edit name..."),
                               tr("Enter new %1 name.").arg(itemtype), QLineEdit::Normal, name);
  if (name.isEmpty()) {
    return false;
  }

  project = CGisWorkspace::self().selectProject(false);
  return nullptr != project;
}

IGisItem* IGisItem::newGisItem(quint32 type, quint64 id, QSqlDatabase& db, IGisProject* project) {
  IGisItem* item = nullptr;

  // load item from database
  switch (type) {
    case IGisItem::eTypeWpt:
      item = new CGisItemWpt(id, db, project);
      break;

    case IGisItem::eTypeTrk:
      item = new CGisItemTrk(id, db, project);
      break;

    case IGisItem::eTypeRte:
      item = new CGisItemRte(id, db, project);
      break;

    case IGisItem::eTypeOvl:
      item = new CGisItemOvlArea(id, db, project);
      break;

    default:;
  }

  return item;
}

void IGisItem::setRating(qreal rating) {
  this->rating = rating;
  updateHistory();
}

QList<QString> IGisItem::getKeywordsSorted() const {
  QList<QString> sortedKeywords = keywords.values();
  std::sort(sortedKeywords.begin(), sortedKeywords.end(), sortByString);
  return sortedKeywords;
}

void IGisItem::addKeywords(const QSet<QString>& otherKeywords) {
  keywords.unite(otherKeywords);
  updateHistory();
}

void IGisItem::removeKeywords(const QSet<QString>& otherKeywords) {
  keywords.subtract(otherKeywords);
  updateHistory();
}

const QString IGisItem::getRatingKeywordInfo() const {
  QString str = "";
  if (getRating() > 0) {
    str += "<br/>\n";
    str += tr("Rating: ") + QString::number(getRating());
  }
  if (!getKeywordsSorted().isEmpty()) {
    str += "<br/>\n";
    str += tr("Keywords: ") + QStringList(getKeywordsSorted()).join(", ");
  }
  return str;
}
