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 }