Kstars

horizonmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "horizonmanager.h"
8
9#include "kstars.h"
10#include "kstarsdata.h"
11#include "linelist.h"
12#include "ksnotification.h"
13#include "Options.h"
14#include "skymap.h"
15#include "projections/projector.h"
16#include "skycomponents/artificialhorizoncomponent.h"
17#include "skycomponents/skymapcomposite.h"
18
19#include <QStandardItemModel>
20
21#include <kstars_debug.h>
22
23#define MIN_NUMBER_POINTS 2
24
25HorizonManagerUI::HorizonManagerUI(QWidget *p) : QFrame(p)
26{
27 setupUi(this);
28}
29
31{
32#ifdef Q_OS_OSX
34#endif
35 ui = new HorizonManagerUI(this);
36
37 ui->setStyleSheet("QPushButton:checked { background-color: red; }");
38
39 ui->addRegionB->setIcon(QIcon::fromTheme("list-add"));
40 ui->addPointB->setIcon(QIcon::fromTheme("list-add"));
41 ui->removeRegionB->setIcon(QIcon::fromTheme("list-remove"));
42 ui->toggleCeilingB->setIcon(QIcon::fromTheme("window"));
43 ui->removePointB->setIcon(QIcon::fromTheme("list-remove"));
44 ui->clearPointsB->setIcon(QIcon::fromTheme("edit-clear"));
45 ui->saveB->setIcon(QIcon::fromTheme("document-save"));
46 ui->selectPointsB->setIcon(
47 QIcon::fromTheme("snap-orthogonal"));
48
49 ui->tipLabel->setPixmap(
50 (QIcon::fromTheme("help-hint").pixmap(64, 64)));
51
52 ui->regionValidation->setPixmap(
53 QIcon::fromTheme("process-stop").pixmap(32, 32));
54 ui->regionValidation->setToolTip(i18n("Region is invalid."));
55 ui->regionValidation->hide();
56
57 setWindowTitle(i18nc("@title:window", "Artificial Horizon Manager"));
58
59 QVBoxLayout *mainLayout = new QVBoxLayout;
60 mainLayout->addWidget(ui);
61 setLayout(mainLayout);
62
64 mainLayout->addWidget(buttonBox);
65 connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
66 connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotSaveChanges()));
67 connect(buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(slotClosed()));
68
69 selectPoints = false;
70
71 // Set up List view
72 m_RegionsModel = new QStandardItemModel(0, 3, this);
74 << i18n("Region") << i18nc("Azimuth", "Az") << i18nc("Altitude", "Alt"));
75
76 ui->regionsList->setModel(m_RegionsModel);
77
78 ui->pointsList->setModel(m_RegionsModel);
79 ui->pointsList->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
80 ui->pointsList->verticalHeader()->hide();
81 ui->pointsList->setColumnHidden(0, true);
82
83 horizonComponent = KStarsData::Instance()->skyComposite()->artificialHorizon();
84
85 // Get the list
86 const QList<ArtificialHorizonEntity *> *horizonList = horizonComponent->getHorizon().horizonList();
87
88 for (auto &horizon : *horizonList)
89 {
90 QStandardItem *regionItem = new QStandardItem(horizon->region());
91 regionItem->setCheckable(true);
92 regionItem->setCheckState(horizon->enabled() ? Qt::Checked : Qt::Unchecked);
93
94 if (horizon->ceiling())
96 else
98 regionItem->setData(horizon->ceiling(), Qt::UserRole);
99
100 m_RegionsModel->appendRow(regionItem);
101
102 SkyList *points = horizon->list()->points();
103
104 for (auto &p : *points)
105 {
107 pointsList << new QStandardItem("") << new QStandardItem(p->az().toDMSString())
108 << new QStandardItem(p->alt().toDMSString());
109 regionItem->appendRow(pointsList);
110 }
111 }
112
113 ui->removeRegionB->setEnabled(true);
114 ui->toggleCeilingB->setEnabled(true);
115
116 connect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
117
118 //Connect buttons
119 connect(ui->addRegionB, SIGNAL(clicked()), this, SLOT(slotAddRegion()));
120 connect(ui->removeRegionB, SIGNAL(clicked()), this, SLOT(slotRemoveRegion()));
121 connect(ui->toggleCeilingB, SIGNAL(clicked()), this, SLOT(slotToggleCeiling()));
122
123 connect(ui->regionsList, SIGNAL(clicked(QModelIndex)), this, SLOT(slotSetShownRegion(QModelIndex)));
124
125 connect(ui->addPointB, SIGNAL(clicked()), this, SLOT(slotAddPoint()));
126 connect(ui->removePointB, SIGNAL(clicked()), this, SLOT(slotRemovePoint()));
127 connect(ui->clearPointsB, SIGNAL(clicked()), this, SLOT(clearPoints()));
128 connect(ui->selectPointsB, SIGNAL(clicked(bool)), this, SLOT(setSelectPoints(bool)));
129
130 connect(ui->pointsList->selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)),
131 this, SLOT(slotCurrentPointChanged(QModelIndex, QModelIndex)));
132
133 connect(ui->saveB, SIGNAL(clicked()), this, SLOT(slotSaveChanges()));
134
135 if (horizonList->count() > 0)
136 {
137 ui->regionsList->selectionModel()->setCurrentIndex(m_RegionsModel->index(0, 0),
139 showRegion(0);
140 }
141}
142
143// If the user hit's the 'X', still want to remove the live preview.
144void HorizonManager::closeEvent(QCloseEvent *event)
145{
147 slotClosed();
148}
149
150// This gets the live preview to be shown when the window is shown.
151void HorizonManager::showEvent(QShowEvent *event)
152{
154 QStandardItem *regionItem = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
155 if (regionItem)
156 {
157 setupLivePreview(regionItem);
158 SkyMap::Instance()->forceUpdateNow();
159 }
160}
161
162// Highlights the current point.
163void HorizonManager::slotCurrentPointChanged(const QModelIndex &selected, const QModelIndex &deselected)
164{
166 if (livePreview.get() != nullptr &&
167 selected.row() >= 0 &&
168 selected.row() < livePreview->points()->size())
169 horizonComponent->setSelectedPreviewPoint(selected.row());
170 else
171 horizonComponent->setSelectedPreviewPoint(-1);
172 SkyMap::Instance()->forceUpdateNow();
173}
174
175// Controls the UI validation check-mark, which indicates if the current
176// region is valid or not.
177void HorizonManager::setupValidation(int region)
178{
179 QStandardItem *regionItem = m_RegionsModel->item(region, 0);
180
181 if (regionItem && regionItem->rowCount() >= MIN_NUMBER_POINTS)
182 {
183 if (validate(region))
184 {
185 ui->regionValidation->setPixmap(
186 QIcon::fromTheme("dialog-ok").pixmap(32, 32));
187 ui->regionValidation->setEnabled(true);
188 ui->regionValidation->setToolTip(i18n("Region is valid"));
189 }
190 else
191 {
192 ui->regionValidation->setPixmap(
193 QIcon::fromTheme("process-stop").pixmap(32, 32));
194 ui->regionValidation->setEnabled(false);
195 ui->regionValidation->setToolTip(i18n("Region is invalid."));
196 }
197
198 ui->regionValidation->show();
199 }
200 else
201 ui->regionValidation->hide();
202}
203
204void HorizonManager::showRegion(int regionID)
205{
206 if (regionID < 0 || regionID >= m_RegionsModel->rowCount())
207 return;
208 else
209 {
210 ui->pointsList->setRootIndex(m_RegionsModel->index(regionID, 0));
211 ui->pointsList->setColumnHidden(0, true);
212
213 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
214
215 if (regionItem->rowCount() > 0)
216 ui->pointsList->setCurrentIndex(regionItem->child(regionItem->rowCount() - 1, 0)->index());
217 else
218 // Invalid index.
219 ui->pointsList->setCurrentIndex(QModelIndex());
220
221 setupValidation(regionID);
222
223 ui->addPointB->setEnabled(true);
224 ui->removePointB->setEnabled(true);
225 ui->selectPointsB->setEnabled(true);
226 ui->clearPointsB->setEnabled(true);
227
228 if (regionItem != nullptr)
229 {
230 setupLivePreview(regionItem);
231 SkyMap::Instance()->forceUpdateNow();
232 }
233 }
234
235 ui->saveB->setEnabled(true);
236}
237
238bool HorizonManager::validate(int regionID)
239{
240 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
241
242 if (regionItem == nullptr || regionItem->rowCount() < MIN_NUMBER_POINTS)
243 return false;
244
245 for (int i = 0; i < regionItem->rowCount(); i++)
246 {
247 dms az = dms::fromString(regionItem->child(i, 1)->data(Qt::DisplayRole).toString(), true);
248 dms alt = dms::fromString(regionItem->child(i, 2)->data(Qt::DisplayRole).toString(), true);
249
250 if (std::isnan(az.Degrees()) || std::isnan(alt.Degrees()))
251 return false;
252 }
253
254 return true;
255}
256
257void HorizonManager::removeEmptyRows(int regionID)
258{
259 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
260
261 if (regionItem == nullptr)
262 return;
263
265 for (int i = 0; i < regionItem->rowCount(); i++)
266 {
267 dms az = dms::fromString(regionItem->child(i, 1)->data(Qt::DisplayRole).toString(), true);
268 dms alt = dms::fromString(regionItem->child(i, 2)->data(Qt::DisplayRole).toString(), true);
269
270 if (std::isnan(az.Degrees()) || std::isnan(alt.Degrees()))
271 emptyRows.append(i);
272 }
273 std::sort(emptyRows.begin(), emptyRows.end(), [](int a, int b) -> bool
274 {
275 return a > b;
276 });
277 for (int i = 0; i < emptyRows.size(); ++i)
278 regionItem->removeRow(emptyRows[i]);
279 return;
280}
281
283{
284 terminateLivePreview();
285
286 setPointSelection(false);
287
288 QStandardItem *regionItem = new QStandardItem(i18n("Region %1", m_RegionsModel->rowCount() + 1));
289 regionItem->setCheckable(true);
290 regionItem->setCheckState(Qt::Checked);
291 m_RegionsModel->appendRow(regionItem);
292
293 QModelIndex index = regionItem->index();
294 ui->regionsList->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
295
296 showRegion(m_RegionsModel->rowCount() - 1);
297}
298
299void HorizonManager::slotToggleCeiling()
300{
301 int regionID = ui->regionsList->currentIndex().row();
302 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
303 if(!regionItem)
304 return;
305
306 if(!regionItem)
307 return;
308 bool turnCeilingOn = !regionItem->data(Qt::UserRole).toBool();
309 if (turnCeilingOn)
310 {
312 regionItem->setData(true, Qt::UserRole);
313 }
314 else
315 {
317 regionItem->setData(false, Qt::UserRole);
318 }
319}
320
322{
323 terminateLivePreview();
324
325 setPointSelection(false);
326
327 int regionID = ui->regionsList->currentIndex().row();
328 deleteRegion(regionID);
329
330 if (regionID > 0)
331 showRegion(regionID - 1);
332 else if (m_RegionsModel->rowCount() == 0)
333 {
334 ui->regionValidation->hide();
335 m_RegionsModel->clear();
336 }
337}
338
339void HorizonManager::deleteRegion(int regionID)
340{
341 if (regionID == -1)
342 return;
343
344 if (regionID < m_RegionsModel->rowCount())
345 {
346 horizonComponent->removeRegion(m_RegionsModel->item(regionID, 0)->data(Qt::DisplayRole).toString());
347 m_RegionsModel->removeRow(regionID);
348 SkyMap::Instance()->forceUpdate();
349 }
350}
351
352void HorizonManager::slotClosed()
353{
354 setSelectPoints(false);
355 terminateLivePreview();
356 SkyMap::Instance()->forceUpdate();
357}
358
359void HorizonManager::slotSaveChanges()
360{
361 terminateLivePreview();
362 setPointSelection(false);
363
364 for (int i = 0; i < m_RegionsModel->rowCount(); i++)
365 {
366 removeEmptyRows(i);
367 if (validate(i) == false)
368 {
369 KSNotification::error(i18n("%1 region is invalid.",
370 m_RegionsModel->item(i, 0)->data(Qt::DisplayRole).toString()));
371 return;
372 }
373 }
374
375 for (int i = 0; i < m_RegionsModel->rowCount(); i++)
376 {
377 QStandardItem *regionItem = m_RegionsModel->item(i, 0);
378 QString regionName = regionItem->data(Qt::DisplayRole).toString();
379
380 horizonComponent->removeRegion(regionName);
381
382 std::shared_ptr<LineList> list(new LineList());
383 dms az, alt;
384 std::shared_ptr<SkyPoint> p;
385
386 for (int j = 0; j < regionItem->rowCount(); j++)
387 {
388 az = dms::fromString(regionItem->child(j, 1)->data(Qt::DisplayRole).toString(), true);
389 alt = dms::fromString(regionItem->child(j, 2)->data(Qt::DisplayRole).toString(), true);
390 if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees())) continue;
391
392 p.reset(new SkyPoint());
393 p->setAz(az);
394 p->setAlt(alt);
395 p->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
396
397 list->append(p);
398 }
399
400 const bool ceiling = regionItem->data(Qt::UserRole).toBool();
401 horizonComponent->addRegion(regionName, regionItem->checkState() == Qt::Checked ? true : false, list, ceiling);
402 }
403
404 horizonComponent->save();
405
406 SkyMap::Instance()->forceUpdateNow();
407}
408
409void HorizonManager::slotSetShownRegion(QModelIndex idx)
410{
411 showRegion(idx.row());
412}
413
414// Copies values from the model to the livePreview, for the passed in region,
415// and passes the livePreview to the horizonComponent, which renders the live preview.
416void HorizonManager::setupLivePreview(QStandardItem * region)
417{
418 if (region == nullptr) return;
419 livePreview.reset(new LineList());
420 const int numPoints = region->rowCount();
421 for (int i = 0; i < numPoints; i++)
422 {
423 QStandardItem *azItem = region->child(i, 1);
424 QStandardItem *altItem = region->child(i, 2);
425
426 const dms az = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
427 const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
428 // Don't render points with bad values.
429 if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees()))
430 continue;
431
432 std::shared_ptr<SkyPoint> point(new SkyPoint());
433 point->setAz(az);
434 point->setAlt(alt);
435 point->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
436
437 livePreview->append(point);
438 }
439
440 horizonComponent->setLivePreview(livePreview);
441}
442
443void HorizonManager::addPoint(SkyPoint *skyPoint)
444{
445 QStandardItem *region = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
446 if (region == nullptr)
447 return;
448
449 // Add the point after the current index in pointsList (row + 1).
450 // If there is no current index, or if somehow (shouldn't happen)
451 // the current index is larger than the list size, insert the point at the end
452 int row = ui->pointsList->currentIndex().row();
453 if ((row < 0) || (row >= region->rowCount()))
454 row = region->rowCount();
455 else row = row + 1;
456
458 pointsList << new QStandardItem("") << new QStandardItem("") << new QStandardItem("");
459
460 region->insertRow(row, pointsList);
461 auto index = region->child(row, 0)->index();
462 ui->pointsList->setCurrentIndex(index);
463
464 m_RegionsModel->setHorizontalHeaderLabels(QStringList()
465 << i18n("Region") << i18nc("Azimuth", "Az") << i18nc("Altitude", "Alt"));
466 ui->pointsList->setColumnHidden(0, true);
467 ui->pointsList->setRootIndex(region->index());
468
469 // If a point was supplied (i.e. the user clicked on the SkyMap, as opposed to
470 // just clicking the addPoint button), then set up its coordinates.
471 if (skyPoint != nullptr)
472 {
473 QStandardItem *az = region->child(row, 1);
474 QStandardItem *alt = region->child(row, 2);
475
476 az->setData(skyPoint->az().toDMSString(), Qt::DisplayRole);
477 alt->setData(skyPoint->alt().toDMSString(), Qt::DisplayRole);
478
479 setupLivePreview(region);
480 slotCurrentPointChanged(ui->pointsList->currentIndex(), ui->pointsList->currentIndex());
481 }
482}
483
484// Called when the user clicks on the SkyMap to add a new point.
485void HorizonManager::addSkyPoint(SkyPoint * skypoint)
486{
487 if (selectPoints == false)
488 return;
489 // Make a copy. This point wasn't staying stable in UI tests.
490 SkyPoint pt = *skypoint;
491 addPoint(&pt);
492}
493
494// Called when the user clicks on the addPoint button.
495void HorizonManager::slotAddPoint()
496{
497 addPoint(nullptr);
498}
499
500void HorizonManager::slotRemovePoint()
501{
502 int regionID = ui->regionsList->currentIndex().row();
503 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
504 if (regionItem == nullptr)
505 return;
506
507 int row = ui->pointsList->currentIndex().row();
508 if (row == -1)
509 row = regionItem->rowCount() - 1;
510 regionItem->removeRow(row);
511
512 setupValidation(regionID);
513
514 if (livePreview.get() && row < livePreview->points()->count())
515 {
516 livePreview->points()->takeAt(row);
517
518 if (livePreview->points()->isEmpty())
519 terminateLivePreview();
520 else
521 SkyMap::Instance()->forceUpdateNow();
522 }
523}
524
525void HorizonManager::clearPoints()
526{
527 QStandardItem *regionItem = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
528
529 if (regionItem)
530 {
531 regionItem->removeRows(0, regionItem->rowCount());
532
533 horizonComponent->removeRegion(regionItem->data(Qt::DisplayRole).toString(), true);
534
535 ui->regionValidation->hide();
536 }
537
538 terminateLivePreview();
539}
540
541void HorizonManager::setSelectPoints(bool enable)
542{
543 selectPoints = enable;
544 ui->selectPointsB->clearFocus();
545}
546
547void HorizonManager::verifyItemValue(QStandardItem * item)
548{
549 bool azOK = true, altOK = true;
550
551 if (item->column() >= 1)
552 {
553 QStandardItem *parent = item->parent();
554
555 dms azAngle = dms::fromString(parent->child(item->row(), 1)->data(Qt::DisplayRole).toString(), true);
556 dms altAngle = dms::fromString(parent->child(item->row(), 2)->data(Qt::DisplayRole).toString(), true);
557
558 if (std::isnan(azAngle.Degrees()))
559 azOK = false;
560 if (std::isnan(altAngle.Degrees()))
561 altOK = false;
562
563 if ((item->column() == 1 && azOK == false) || (item->column() == 2 && altOK == false))
564
565 {
566 KSNotification::error(i18n("Invalid angle value: %1", item->data(Qt::DisplayRole).toString()));
567 disconnect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
569 connect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
570 return;
571 }
572 else if (azOK && altOK)
573 {
574 setupLivePreview(item->parent());
575 setupValidation(ui->regionsList->currentIndex().row());
576 SkyMap::Instance()->forceUpdateNow();
577 }
578 }
579}
580
581void HorizonManager::terminateLivePreview()
582{
583 if (!livePreview.get())
584 return;
585
586 livePreview.reset();
587 horizonComponent->setLivePreview(livePreview);
588}
589
590void HorizonManager::setPointSelection(bool enable)
591{
592 selectPoints = enable;
593 ui->selectPointsB->setChecked(enable);
594}
void slotRemoveRegion()
Delete region.
HorizonManager(QWidget *ks)
Constructor.
void slotAddRegion()
Add region.
A simple data container used by LineListIndex.
Definition linelist.h:25
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const dms & az() const
Definition skypoint.h:275
const dms & alt() const
Definition skypoint.h:281
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
const double & Degrees() const
Definition dms.h:141
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
GeoCoordinates geo(const QVariant &location)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
bool removeRow(int row, const QModelIndex &parent)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
virtual void reject()
void rejected()
QPushButton * button(StandardButton which) const const
QIcon fromTheme(const QString &name)
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
QStandardItem * child(int row, int column) const const
int column() const const
virtual QVariant data(int role) const const
QModelIndex index() const const
void insertRow(int row, QStandardItem *item)
QStandardItem * parent() const const
int row() const const
int rowCount() const const
virtual void setData(const QVariant &value, int role)
void appendRow(QStandardItem *item)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
QStandardItem * item(int row, int column) const const
virtual int rowCount(const QModelIndex &parent) const const override
void setHorizontalHeaderLabels(const QStringList &labels)
DecorationRole
void clearFocus()
void setEnabled(bool)
virtual bool event(QEvent *event) override
void hide()
void setLayout(QLayout *layout)
void show()
virtual void showEvent(QShowEvent *event)
void setStyleSheet(const QString &styleSheet)
void setToolTip(const QString &)
void setWindowFlags(Qt::WindowFlags type)
void setWindowTitle(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:04 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.