File indexing completed on 2024-04-14 03:43:20

0001 /*
0002     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "horizonmanager.h"
0008 
0009 #include "kstars.h"
0010 #include "kstarsdata.h"
0011 #include "linelist.h"
0012 #include "ksnotification.h"
0013 #include "Options.h"
0014 #include "skymap.h"
0015 #include "projections/projector.h"
0016 #include "skycomponents/artificialhorizoncomponent.h"
0017 #include "skycomponents/skymapcomposite.h"
0018 
0019 #include <QStandardItemModel>
0020 
0021 #include <kstars_debug.h>
0022 
0023 #define MIN_NUMBER_POINTS 2
0024 
0025 HorizonManagerUI::HorizonManagerUI(QWidget *p) : QFrame(p)
0026 {
0027     setupUi(this);
0028 }
0029 
0030 HorizonManager::HorizonManager(QWidget *w) : QDialog(w)
0031 {
0032 #ifdef Q_OS_OSX
0033     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0034 #endif
0035     ui = new HorizonManagerUI(this);
0036 
0037     ui->setStyleSheet("QPushButton:checked { background-color: red; }");
0038 
0039     ui->addRegionB->setIcon(QIcon::fromTheme("list-add"));
0040     ui->addPointB->setIcon(QIcon::fromTheme("list-add"));
0041     ui->removeRegionB->setIcon(QIcon::fromTheme("list-remove"));
0042     ui->toggleCeilingB->setIcon(QIcon::fromTheme("window"));
0043     ui->removePointB->setIcon(QIcon::fromTheme("list-remove"));
0044     ui->clearPointsB->setIcon(QIcon::fromTheme("edit-clear"));
0045     ui->saveB->setIcon(QIcon::fromTheme("document-save"));
0046     ui->selectPointsB->setIcon(
0047         QIcon::fromTheme("snap-orthogonal"));
0048 
0049     ui->tipLabel->setPixmap(
0050         (QIcon::fromTheme("help-hint").pixmap(64, 64)));
0051 
0052     ui->regionValidation->setPixmap(
0053         QIcon::fromTheme("process-stop").pixmap(32, 32));
0054     ui->regionValidation->setToolTip(i18n("Region is invalid."));
0055     ui->regionValidation->hide();
0056 
0057     setWindowTitle(i18nc("@title:window", "Artificial Horizon Manager"));
0058 
0059     QVBoxLayout *mainLayout = new QVBoxLayout;
0060     mainLayout->addWidget(ui);
0061     setLayout(mainLayout);
0062 
0063     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close);
0064     mainLayout->addWidget(buttonBox);
0065     connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
0066     connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotSaveChanges()));
0067     connect(buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(slotClosed()));
0068 
0069     selectPoints = false;
0070 
0071     // Set up List view
0072     m_RegionsModel = new QStandardItemModel(0, 3, this);
0073     m_RegionsModel->setHorizontalHeaderLabels(QStringList()
0074             << i18n("Region") << i18nc("Azimuth", "Az") << i18nc("Altitude", "Alt"));
0075 
0076     ui->regionsList->setModel(m_RegionsModel);
0077 
0078     ui->pointsList->setModel(m_RegionsModel);
0079     ui->pointsList->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
0080     ui->pointsList->verticalHeader()->hide();
0081     ui->pointsList->setColumnHidden(0, true);
0082 
0083     horizonComponent = KStarsData::Instance()->skyComposite()->artificialHorizon();
0084 
0085     // Get the list
0086     const QList<ArtificialHorizonEntity *> *horizonList = horizonComponent->getHorizon().horizonList();
0087 
0088     for (auto &horizon : *horizonList)
0089     {
0090         QStandardItem *regionItem = new QStandardItem(horizon->region());
0091         regionItem->setCheckable(true);
0092         regionItem->setCheckState(horizon->enabled() ? Qt::Checked : Qt::Unchecked);
0093 
0094         if (horizon->ceiling())
0095             regionItem->setData(QIcon::fromTheme("window"), Qt::DecorationRole);
0096         else
0097             regionItem->setData(QIcon(), Qt::DecorationRole);
0098         regionItem->setData(horizon->ceiling(), Qt::UserRole);
0099 
0100         m_RegionsModel->appendRow(regionItem);
0101 
0102         SkyList *points = horizon->list()->points();
0103 
0104         for (auto &p : *points)
0105         {
0106             QList<QStandardItem *> pointsList;
0107             pointsList << new QStandardItem("") << new QStandardItem(p->az().toDMSString())
0108                        << new QStandardItem(p->alt().toDMSString());
0109             regionItem->appendRow(pointsList);
0110         }
0111     }
0112 
0113     ui->removeRegionB->setEnabled(true);
0114     ui->toggleCeilingB->setEnabled(true);
0115 
0116     connect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
0117 
0118     //Connect buttons
0119     connect(ui->addRegionB, SIGNAL(clicked()), this, SLOT(slotAddRegion()));
0120     connect(ui->removeRegionB, SIGNAL(clicked()), this, SLOT(slotRemoveRegion()));
0121     connect(ui->toggleCeilingB, SIGNAL(clicked()), this, SLOT(slotToggleCeiling()));
0122 
0123     connect(ui->regionsList, SIGNAL(clicked(QModelIndex)), this, SLOT(slotSetShownRegion(QModelIndex)));
0124 
0125     connect(ui->addPointB, SIGNAL(clicked()), this, SLOT(slotAddPoint()));
0126     connect(ui->removePointB, SIGNAL(clicked()), this, SLOT(slotRemovePoint()));
0127     connect(ui->clearPointsB, SIGNAL(clicked()), this, SLOT(clearPoints()));
0128     connect(ui->selectPointsB, SIGNAL(clicked(bool)), this, SLOT(setSelectPoints(bool)));
0129 
0130     connect(ui->pointsList->selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)),
0131             this, SLOT(slotCurrentPointChanged(QModelIndex, QModelIndex)));
0132 
0133     connect(ui->saveB, SIGNAL(clicked()), this, SLOT(slotSaveChanges()));
0134 
0135     if (horizonList->count() > 0)
0136     {
0137         ui->regionsList->selectionModel()->setCurrentIndex(m_RegionsModel->index(0, 0),
0138                 QItemSelectionModel::SelectCurrent);
0139         showRegion(0);
0140     }
0141 }
0142 
0143 // If the user hit's the 'X', still want to remove the live preview.
0144 void HorizonManager::closeEvent(QCloseEvent *event)
0145 {
0146     Q_UNUSED(event);
0147     slotClosed();
0148 }
0149 
0150 // This gets the live preview to be shown when the window is shown.
0151 void HorizonManager::showEvent(QShowEvent *event)
0152 {
0153     QWidget::showEvent( event );
0154     QStandardItem *regionItem = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
0155     if (regionItem)
0156     {
0157         setupLivePreview(regionItem);
0158         SkyMap::Instance()->forceUpdateNow();
0159     }
0160 }
0161 
0162 // Highlights the current point.
0163 void HorizonManager::slotCurrentPointChanged(const QModelIndex &selected, const QModelIndex &deselected)
0164 {
0165     Q_UNUSED(deselected);
0166     if (livePreview.get() != nullptr &&
0167             selected.row() >= 0 &&
0168             selected.row() < livePreview->points()->size())
0169         horizonComponent->setSelectedPreviewPoint(selected.row());
0170     else
0171         horizonComponent->setSelectedPreviewPoint(-1);
0172     SkyMap::Instance()->forceUpdateNow();
0173 }
0174 
0175 // Controls the UI validation check-mark, which indicates if the current
0176 // region is valid or not.
0177 void HorizonManager::setupValidation(int region)
0178 {
0179     QStandardItem *regionItem = m_RegionsModel->item(region, 0);
0180 
0181     if (regionItem && regionItem->rowCount() >= MIN_NUMBER_POINTS)
0182     {
0183         if (validate(region))
0184         {
0185             ui->regionValidation->setPixmap(
0186                 QIcon::fromTheme("dialog-ok").pixmap(32, 32));
0187             ui->regionValidation->setEnabled(true);
0188             ui->regionValidation->setToolTip(i18n("Region is valid"));
0189         }
0190         else
0191         {
0192             ui->regionValidation->setPixmap(
0193                 QIcon::fromTheme("process-stop").pixmap(32, 32));
0194             ui->regionValidation->setEnabled(false);
0195             ui->regionValidation->setToolTip(i18n("Region is invalid."));
0196         }
0197 
0198         ui->regionValidation->show();
0199     }
0200     else
0201         ui->regionValidation->hide();
0202 }
0203 
0204 void HorizonManager::showRegion(int regionID)
0205 {
0206     if (regionID < 0 || regionID >= m_RegionsModel->rowCount())
0207         return;
0208     else
0209     {
0210         ui->pointsList->setRootIndex(m_RegionsModel->index(regionID, 0));
0211         ui->pointsList->setColumnHidden(0, true);
0212 
0213         QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
0214 
0215         if (regionItem->rowCount() > 0)
0216             ui->pointsList->setCurrentIndex(regionItem->child(regionItem->rowCount() - 1, 0)->index());
0217         else
0218             // Invalid index.
0219             ui->pointsList->setCurrentIndex(QModelIndex());
0220 
0221         setupValidation(regionID);
0222 
0223         ui->addPointB->setEnabled(true);
0224         ui->removePointB->setEnabled(true);
0225         ui->selectPointsB->setEnabled(true);
0226         ui->clearPointsB->setEnabled(true);
0227 
0228         if (regionItem != nullptr)
0229         {
0230             setupLivePreview(regionItem);
0231             SkyMap::Instance()->forceUpdateNow();
0232         }
0233     }
0234 
0235     ui->saveB->setEnabled(true);
0236 }
0237 
0238 bool HorizonManager::validate(int regionID)
0239 {
0240     QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
0241 
0242     if (regionItem == nullptr || regionItem->rowCount() < MIN_NUMBER_POINTS)
0243         return false;
0244 
0245     for (int i = 0; i < regionItem->rowCount(); i++)
0246     {
0247         dms az  = dms::fromString(regionItem->child(i, 1)->data(Qt::DisplayRole).toString(), true);
0248         dms alt = dms::fromString(regionItem->child(i, 2)->data(Qt::DisplayRole).toString(), true);
0249 
0250         if (std::isnan(az.Degrees()) || std::isnan(alt.Degrees()))
0251             return false;
0252     }
0253 
0254     return true;
0255 }
0256 
0257 void HorizonManager::removeEmptyRows(int regionID)
0258 {
0259     QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
0260 
0261     if (regionItem == nullptr)
0262         return;
0263 
0264     QList<int> emptyRows;
0265     for (int i = 0; i < regionItem->rowCount(); i++)
0266     {
0267         dms az  = dms::fromString(regionItem->child(i, 1)->data(Qt::DisplayRole).toString(), true);
0268         dms alt = dms::fromString(regionItem->child(i, 2)->data(Qt::DisplayRole).toString(), true);
0269 
0270         if (std::isnan(az.Degrees()) || std::isnan(alt.Degrees()))
0271             emptyRows.append(i);
0272     }
0273     std::sort(emptyRows.begin(), emptyRows.end(), [](int a, int b) -> bool
0274     {
0275         return a > b;
0276     });
0277     for (int i = 0; i < emptyRows.size(); ++i)
0278         regionItem->removeRow(emptyRows[i]);
0279     return;
0280 }
0281 
0282 void HorizonManager::slotAddRegion()
0283 {
0284     terminateLivePreview();
0285 
0286     setPointSelection(false);
0287 
0288     QStandardItem *regionItem = new QStandardItem(i18n("Region %1", m_RegionsModel->rowCount() + 1));
0289     regionItem->setCheckable(true);
0290     regionItem->setCheckState(Qt::Checked);
0291     m_RegionsModel->appendRow(regionItem);
0292 
0293     QModelIndex index = regionItem->index();
0294     ui->regionsList->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
0295 
0296     showRegion(m_RegionsModel->rowCount() - 1);
0297 }
0298 
0299 void HorizonManager::slotToggleCeiling()
0300 {
0301     int regionID = ui->regionsList->currentIndex().row();
0302     QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
0303     if(!regionItem)
0304         return;
0305 
0306     if(!regionItem)
0307         return;
0308     bool turnCeilingOn = !regionItem->data(Qt::UserRole).toBool();
0309     if (turnCeilingOn)
0310     {
0311         regionItem->setData(QIcon::fromTheme("window"), Qt::DecorationRole);
0312         regionItem->setData(true, Qt::UserRole);
0313     }
0314     else
0315     {
0316         regionItem->setData(QIcon(), Qt::DecorationRole);
0317         regionItem->setData(false, Qt::UserRole);
0318     }
0319 }
0320 
0321 void HorizonManager::slotRemoveRegion()
0322 {
0323     terminateLivePreview();
0324 
0325     setPointSelection(false);
0326 
0327     int regionID = ui->regionsList->currentIndex().row();
0328     deleteRegion(regionID);
0329 
0330     if (regionID > 0)
0331         showRegion(regionID - 1);
0332     else if (m_RegionsModel->rowCount() == 0)
0333     {
0334         ui->regionValidation->hide();
0335         m_RegionsModel->clear();
0336     }
0337 }
0338 
0339 void HorizonManager::deleteRegion(int regionID)
0340 {
0341     if (regionID == -1)
0342         return;
0343 
0344     if (regionID < m_RegionsModel->rowCount())
0345     {
0346         horizonComponent->removeRegion(m_RegionsModel->item(regionID, 0)->data(Qt::DisplayRole).toString());
0347         m_RegionsModel->removeRow(regionID);
0348         SkyMap::Instance()->forceUpdate();
0349     }
0350 }
0351 
0352 void HorizonManager::slotClosed()
0353 {
0354     setSelectPoints(false);
0355     terminateLivePreview();
0356     SkyMap::Instance()->forceUpdate();
0357 }
0358 
0359 void HorizonManager::slotSaveChanges()
0360 {
0361     terminateLivePreview();
0362     setPointSelection(false);
0363 
0364     for (int i = 0; i < m_RegionsModel->rowCount(); i++)
0365     {
0366         removeEmptyRows(i);
0367         if (validate(i) == false)
0368         {
0369             KSNotification::error(i18n("%1 region is invalid.",
0370                                        m_RegionsModel->item(i, 0)->data(Qt::DisplayRole).toString()));
0371             return;
0372         }
0373     }
0374 
0375     for (int i = 0; i < m_RegionsModel->rowCount(); i++)
0376     {
0377         QStandardItem *regionItem = m_RegionsModel->item(i, 0);
0378         QString regionName        = regionItem->data(Qt::DisplayRole).toString();
0379 
0380         horizonComponent->removeRegion(regionName);
0381 
0382         std::shared_ptr<LineList> list(new LineList());
0383         dms az, alt;
0384         std::shared_ptr<SkyPoint> p;
0385 
0386         for (int j = 0; j < regionItem->rowCount(); j++)
0387         {
0388             az  = dms::fromString(regionItem->child(j, 1)->data(Qt::DisplayRole).toString(), true);
0389             alt = dms::fromString(regionItem->child(j, 2)->data(Qt::DisplayRole).toString(), true);
0390             if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees())) continue;
0391 
0392             p.reset(new SkyPoint());
0393             p->setAz(az);
0394             p->setAlt(alt);
0395             p->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0396 
0397             list->append(p);
0398         }
0399 
0400         const bool ceiling = regionItem->data(Qt::UserRole).toBool();
0401         horizonComponent->addRegion(regionName, regionItem->checkState() == Qt::Checked ? true : false, list, ceiling);
0402     }
0403 
0404     horizonComponent->save();
0405 
0406     SkyMap::Instance()->forceUpdateNow();
0407 }
0408 
0409 void HorizonManager::slotSetShownRegion(QModelIndex idx)
0410 {
0411     showRegion(idx.row());
0412 }
0413 
0414 // Copies values from the model to the livePreview, for the passed in region,
0415 // and passes the livePreview to the horizonComponent, which renders the live preview.
0416 void HorizonManager::setupLivePreview(QStandardItem * region)
0417 {
0418     if (region == nullptr) return;
0419     livePreview.reset(new LineList());
0420     const int numPoints = region->rowCount();
0421     for (int i = 0; i < numPoints; i++)
0422     {
0423         QStandardItem *azItem  = region->child(i, 1);
0424         QStandardItem *altItem = region->child(i, 2);
0425 
0426         const dms az  = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
0427         const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
0428         // Don't render points with bad values.
0429         if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees()))
0430             continue;
0431 
0432         std::shared_ptr<SkyPoint> point(new SkyPoint());
0433         point->setAz(az);
0434         point->setAlt(alt);
0435         point->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0436 
0437         livePreview->append(point);
0438     }
0439 
0440     horizonComponent->setLivePreview(livePreview);
0441 }
0442 
0443 void HorizonManager::addPoint(SkyPoint *skyPoint)
0444 {
0445     QStandardItem *region = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
0446     if (region == nullptr)
0447         return;
0448 
0449     // Add the point after the current index in pointsList (row + 1).
0450     // If there is no current index, or if somehow (shouldn't happen)
0451     // the current index is larger than the list size, insert the point at the end
0452     int row = ui->pointsList->currentIndex().row();
0453     if ((row < 0) || (row >= region->rowCount()))
0454         row = region->rowCount();
0455     else row = row + 1;
0456 
0457     QList<QStandardItem *> pointsList;
0458     pointsList << new QStandardItem("") << new QStandardItem("") << new QStandardItem("");
0459 
0460     region->insertRow(row, pointsList);
0461     auto index = region->child(row, 0)->index();
0462     ui->pointsList->setCurrentIndex(index);
0463 
0464     m_RegionsModel->setHorizontalHeaderLabels(QStringList()
0465             << i18n("Region") << i18nc("Azimuth", "Az") << i18nc("Altitude", "Alt"));
0466     ui->pointsList->setColumnHidden(0, true);
0467     ui->pointsList->setRootIndex(region->index());
0468 
0469     // If a point was supplied (i.e. the user clicked on the SkyMap, as opposed to
0470     // just clicking the addPoint button), then set up its coordinates.
0471     if (skyPoint != nullptr)
0472     {
0473         QStandardItem *az  = region->child(row, 1);
0474         QStandardItem *alt = region->child(row, 2);
0475 
0476         az->setData(skyPoint->az().toDMSString(), Qt::DisplayRole);
0477         alt->setData(skyPoint->alt().toDMSString(), Qt::DisplayRole);
0478 
0479         setupLivePreview(region);
0480         slotCurrentPointChanged(ui->pointsList->currentIndex(), ui->pointsList->currentIndex());
0481     }
0482 }
0483 
0484 // Called when the user clicks on the SkyMap to add a new point.
0485 void HorizonManager::addSkyPoint(SkyPoint * skypoint)
0486 {
0487     if (selectPoints == false)
0488         return;
0489     // Make a copy.  This point wasn't staying stable in UI tests.
0490     SkyPoint pt = *skypoint;
0491     addPoint(&pt);
0492 }
0493 
0494 // Called when the user clicks on the addPoint button.
0495 void HorizonManager::slotAddPoint()
0496 {
0497     addPoint(nullptr);
0498 }
0499 
0500 void HorizonManager::slotRemovePoint()
0501 {
0502     int regionID = ui->regionsList->currentIndex().row();
0503     QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
0504     if (regionItem == nullptr)
0505         return;
0506 
0507     int row = ui->pointsList->currentIndex().row();
0508     if (row == -1)
0509         row = regionItem->rowCount() - 1;
0510     regionItem->removeRow(row);
0511 
0512     setupValidation(regionID);
0513 
0514     if (livePreview.get() && row < livePreview->points()->count())
0515     {
0516         livePreview->points()->takeAt(row);
0517 
0518         if (livePreview->points()->isEmpty())
0519             terminateLivePreview();
0520         else
0521             SkyMap::Instance()->forceUpdateNow();
0522     }
0523 }
0524 
0525 void HorizonManager::clearPoints()
0526 {
0527     QStandardItem *regionItem = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
0528 
0529     if (regionItem)
0530     {
0531         regionItem->removeRows(0, regionItem->rowCount());
0532 
0533         horizonComponent->removeRegion(regionItem->data(Qt::DisplayRole).toString(), true);
0534 
0535         ui->regionValidation->hide();
0536     }
0537 
0538     terminateLivePreview();
0539 }
0540 
0541 void HorizonManager::setSelectPoints(bool enable)
0542 {
0543     selectPoints = enable;
0544     ui->selectPointsB->clearFocus();
0545 }
0546 
0547 void HorizonManager::verifyItemValue(QStandardItem * item)
0548 {
0549     bool azOK = true, altOK = true;
0550 
0551     if (item->column() >= 1)
0552     {
0553         QStandardItem *parent = item->parent();
0554 
0555         dms azAngle  = dms::fromString(parent->child(item->row(), 1)->data(Qt::DisplayRole).toString(), true);
0556         dms altAngle = dms::fromString(parent->child(item->row(), 2)->data(Qt::DisplayRole).toString(), true);
0557 
0558         if (std::isnan(azAngle.Degrees()))
0559             azOK = false;
0560         if (std::isnan(altAngle.Degrees()))
0561             altOK = false;
0562 
0563         if ((item->column() == 1 && azOK == false) || (item->column() == 2 && altOK == false))
0564 
0565         {
0566             KSNotification::error(i18n("Invalid angle value: %1", item->data(Qt::DisplayRole).toString()));
0567             disconnect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
0568             item->setData(QVariant(qQNaN()), Qt::DisplayRole);
0569             connect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
0570             return;
0571         }
0572         else if (azOK && altOK)
0573         {
0574             setupLivePreview(item->parent());
0575             setupValidation(ui->regionsList->currentIndex().row());
0576             SkyMap::Instance()->forceUpdateNow();
0577         }
0578     }
0579 }
0580 
0581 void HorizonManager::terminateLivePreview()
0582 {
0583     if (!livePreview.get())
0584         return;
0585 
0586     livePreview.reset();
0587     horizonComponent->setLivePreview(livePreview);
0588 }
0589 
0590 void HorizonManager::setPointSelection(bool enable)
0591 {
0592     selectPoints = enable;
0593     ui->selectPointsB->setChecked(enable);
0594 }