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
HorizonManagerUI(QWidget * p)25 HorizonManagerUI::HorizonManagerUI(QWidget *p) : QFrame(p)
26 {
27 setupUi(this);
28 }
29
HorizonManager(QWidget * w)30 HorizonManager::HorizonManager(QWidget *w) : QDialog(w)
31 {
32 #ifdef Q_OS_OSX
33 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
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
63 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close);
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);
73 m_RegionsModel->setHorizontalHeaderLabels(QStringList()
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())
95 regionItem->setData(QIcon::fromTheme("window"), Qt::DecorationRole);
96 else
97 regionItem->setData(QIcon(), Qt::DecorationRole);
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 {
106 QList<QStandardItem *> pointsList;
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),
138 QItemSelectionModel::SelectCurrent);
139 showRegion(0);
140 }
141 }
142
143 // If the user hit's the 'X', still want to remove the live preview.
closeEvent(QCloseEvent * event)144 void HorizonManager::closeEvent(QCloseEvent *event)
145 {
146 Q_UNUSED(event);
147 slotClosed();
148 }
149
150 // This gets the live preview to be shown when the window is shown.
showEvent(QShowEvent * event)151 void HorizonManager::showEvent(QShowEvent *event)
152 {
153 QWidget::showEvent( event );
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.
slotCurrentPointChanged(const QModelIndex & selected,const QModelIndex & deselected)163 void HorizonManager::slotCurrentPointChanged(const QModelIndex &selected, const QModelIndex &deselected)
164 {
165 Q_UNUSED(deselected);
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.
setupValidation(int region)177 void 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
showRegion(int regionID)204 void 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
validate(int regionID)238 bool 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
removeEmptyRows(int regionID)257 void HorizonManager::removeEmptyRows(int regionID)
258 {
259 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
260
261 if (regionItem == nullptr)
262 return;
263
264 QList<int> emptyRows;
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
slotAddRegion()282 void HorizonManager::slotAddRegion()
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
slotToggleCeiling()299 void HorizonManager::slotToggleCeiling()
300 {
301 int regionID = ui->regionsList->currentIndex().row();
302 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
303
304 bool turnCeilingOn = !regionItem->data(Qt::UserRole).toBool();
305 if (turnCeilingOn)
306 {
307 regionItem->setData(QIcon::fromTheme("window"), Qt::DecorationRole);
308 regionItem->setData(true, Qt::UserRole);
309 }
310 else
311 {
312 regionItem->setData(QIcon(), Qt::DecorationRole);
313 regionItem->setData(false, Qt::UserRole);
314 }
315 }
316
slotRemoveRegion()317 void HorizonManager::slotRemoveRegion()
318 {
319 terminateLivePreview();
320
321 setPointSelection(false);
322
323 int regionID = ui->regionsList->currentIndex().row();
324 deleteRegion(regionID);
325
326 if (regionID > 0)
327 showRegion(regionID - 1);
328 else if (m_RegionsModel->rowCount() == 0)
329 {
330 ui->regionValidation->hide();
331 m_RegionsModel->clear();
332 }
333 }
334
deleteRegion(int regionID)335 void HorizonManager::deleteRegion(int regionID)
336 {
337 if (regionID == -1)
338 return;
339
340 if (regionID < m_RegionsModel->rowCount())
341 {
342 horizonComponent->removeRegion(m_RegionsModel->item(regionID, 0)->data(Qt::DisplayRole).toString());
343 m_RegionsModel->removeRow(regionID);
344 SkyMap::Instance()->forceUpdate();
345 }
346 }
347
slotClosed()348 void HorizonManager::slotClosed()
349 {
350 setSelectPoints(false);
351 terminateLivePreview();
352 SkyMap::Instance()->forceUpdate();
353 }
354
slotSaveChanges()355 void HorizonManager::slotSaveChanges()
356 {
357 terminateLivePreview();
358 setPointSelection(false);
359
360 for (int i = 0; i < m_RegionsModel->rowCount(); i++)
361 {
362 removeEmptyRows(i);
363 if (validate(i) == false)
364 {
365 KSNotification::error(i18n("%1 region is invalid.",
366 m_RegionsModel->item(i, 0)->data(Qt::DisplayRole).toString()));
367 return;
368 }
369 }
370
371 for (int i = 0; i < m_RegionsModel->rowCount(); i++)
372 {
373 QStandardItem *regionItem = m_RegionsModel->item(i, 0);
374 QString regionName = regionItem->data(Qt::DisplayRole).toString();
375
376 horizonComponent->removeRegion(regionName);
377
378 std::shared_ptr<LineList> list(new LineList());
379 dms az, alt;
380 std::shared_ptr<SkyPoint> p;
381
382 for (int j = 0; j < regionItem->rowCount(); j++)
383 {
384 az = dms::fromString(regionItem->child(j, 1)->data(Qt::DisplayRole).toString(), true);
385 alt = dms::fromString(regionItem->child(j, 2)->data(Qt::DisplayRole).toString(), true);
386 if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees())) continue;
387
388 p.reset(new SkyPoint());
389 p->setAz(az);
390 p->setAlt(alt);
391 p->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
392
393 list->append(p);
394 }
395
396 const bool ceiling = regionItem->data(Qt::UserRole).toBool();
397 horizonComponent->addRegion(regionName, regionItem->checkState() == Qt::Checked ? true : false, list, ceiling);
398 }
399
400 horizonComponent->save();
401
402 SkyMap::Instance()->forceUpdateNow();
403 }
404
slotSetShownRegion(QModelIndex idx)405 void HorizonManager::slotSetShownRegion(QModelIndex idx)
406 {
407 showRegion(idx.row());
408 }
409
410 // Copies values from the model to the livePreview, for the passed in region,
411 // and passes the livePreview to the horizonComponent, which renders the live preview.
setupLivePreview(QStandardItem * region)412 void HorizonManager::setupLivePreview(QStandardItem * region)
413 {
414 if (region == nullptr) return;
415 livePreview.reset(new LineList());
416 const int numPoints = region->rowCount();
417 for (int i = 0; i < numPoints; i++)
418 {
419 QStandardItem *azItem = region->child(i, 1);
420 QStandardItem *altItem = region->child(i, 2);
421
422 const dms az = dms::fromString(azItem->data(Qt::DisplayRole).toString(), true);
423 const dms alt = dms::fromString(altItem->data(Qt::DisplayRole).toString(), true);
424 // Don't render points with bad values.
425 if (qIsNaN(az.Degrees()) || qIsNaN(alt.Degrees()))
426 continue;
427
428 std::shared_ptr<SkyPoint> point(new SkyPoint());
429 point->setAz(az);
430 point->setAlt(alt);
431 point->HorizontalToEquatorial(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
432
433 livePreview->append(point);
434 }
435
436 horizonComponent->setLivePreview(livePreview);
437 }
438
addPoint(SkyPoint * skyPoint)439 void HorizonManager::addPoint(SkyPoint *skyPoint)
440 {
441 QStandardItem *region = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
442 if (region == nullptr)
443 return;
444
445 // Add the point after the current index in pointsList (row + 1).
446 // If there is no current index, or if somehow (shouldn't happen)
447 // the current index is larger than the list size, insert the point at the end
448 int row = ui->pointsList->currentIndex().row();
449 if ((row < 0) || (row >= region->rowCount()))
450 row = region->rowCount();
451 else row = row + 1;
452
453 QList<QStandardItem *> pointsList;
454 pointsList << new QStandardItem("") << new QStandardItem("") << new QStandardItem("");
455
456 region->insertRow(row, pointsList);
457 auto index = region->child(row, 0)->index();
458 ui->pointsList->setCurrentIndex(index);
459
460 m_RegionsModel->setHorizontalHeaderLabels(QStringList()
461 << i18n("Region") << i18nc("Azimuth", "Az") << i18nc("Altitude", "Alt"));
462 ui->pointsList->setColumnHidden(0, true);
463 ui->pointsList->setRootIndex(region->index());
464
465 // If a point was supplied (i.e. the user clicked on the SkyMap, as opposed to
466 // just clicking the addPoint button), then set up its coordinates.
467 if (skyPoint != nullptr)
468 {
469 QStandardItem *az = region->child(row, 1);
470 QStandardItem *alt = region->child(row, 2);
471
472 az->setData(skyPoint->az().toDMSString(), Qt::DisplayRole);
473 alt->setData(skyPoint->alt().toDMSString(), Qt::DisplayRole);
474
475 setupLivePreview(region);
476 slotCurrentPointChanged(ui->pointsList->currentIndex(), ui->pointsList->currentIndex());
477 }
478 }
479
480 // Called when the user clicks on the SkyMap to add a new point.
addSkyPoint(SkyPoint * skypoint)481 void HorizonManager::addSkyPoint(SkyPoint * skypoint)
482 {
483 if (selectPoints == false)
484 return;
485 // Make a copy. This point wasn't staying stable in UI tests.
486 SkyPoint pt = *skypoint;
487 addPoint(&pt);
488 }
489
490 // Called when the user clicks on the addPoint button.
slotAddPoint()491 void HorizonManager::slotAddPoint()
492 {
493 addPoint(nullptr);
494 }
495
slotRemovePoint()496 void HorizonManager::slotRemovePoint()
497 {
498 int regionID = ui->regionsList->currentIndex().row();
499 QStandardItem *regionItem = m_RegionsModel->item(regionID, 0);
500 if (regionItem == nullptr)
501 return;
502
503 int row = ui->pointsList->currentIndex().row();
504 if (row == -1)
505 row = regionItem->rowCount() - 1;
506 regionItem->removeRow(row);
507
508 setupValidation(regionID);
509
510 if (livePreview.get() && row < livePreview->points()->count())
511 {
512 livePreview->points()->takeAt(row);
513
514 if (livePreview->points()->isEmpty())
515 terminateLivePreview();
516 else
517 SkyMap::Instance()->forceUpdateNow();
518 }
519 }
520
clearPoints()521 void HorizonManager::clearPoints()
522 {
523 QStandardItem *regionItem = m_RegionsModel->item(ui->regionsList->currentIndex().row(), 0);
524
525 if (regionItem)
526 {
527 regionItem->removeRows(0, regionItem->rowCount());
528
529 horizonComponent->removeRegion(regionItem->data(Qt::DisplayRole).toString(), true);
530
531 ui->regionValidation->hide();
532 }
533
534 terminateLivePreview();
535 }
536
setSelectPoints(bool enable)537 void HorizonManager::setSelectPoints(bool enable)
538 {
539 selectPoints = enable;
540 ui->selectPointsB->clearFocus();
541 }
542
verifyItemValue(QStandardItem * item)543 void HorizonManager::verifyItemValue(QStandardItem * item)
544 {
545 bool azOK = true, altOK = true;
546
547 if (item->column() >= 1)
548 {
549 QStandardItem *parent = item->parent();
550
551 dms azAngle = dms::fromString(parent->child(item->row(), 1)->data(Qt::DisplayRole).toString(), true);
552 dms altAngle = dms::fromString(parent->child(item->row(), 2)->data(Qt::DisplayRole).toString(), true);
553
554 if (std::isnan(azAngle.Degrees()))
555 azOK = false;
556 if (std::isnan(altAngle.Degrees()))
557 altOK = false;
558
559 if ((item->column() == 1 && azOK == false) || (item->column() == 2 && altOK == false))
560
561 {
562 KSNotification::error(i18n("Invalid angle value: %1", item->data(Qt::DisplayRole).toString()));
563 disconnect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
564 item->setData(QVariant(qQNaN()), Qt::DisplayRole);
565 connect(m_RegionsModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(verifyItemValue(QStandardItem*)));
566 return;
567 }
568 else if (azOK && altOK)
569 {
570 setupLivePreview(item->parent());
571 setupValidation(ui->regionsList->currentIndex().row());
572 SkyMap::Instance()->forceUpdateNow();
573 }
574 }
575 }
576
terminateLivePreview()577 void HorizonManager::terminateLivePreview()
578 {
579 if (!livePreview.get())
580 return;
581
582 livePreview.reset();
583 horizonComponent->setLivePreview(livePreview);
584 }
585
setPointSelection(bool enable)586 void HorizonManager::setPointSelection(bool enable)
587 {
588 selectPoints = enable;
589 ui->selectPointsB->setChecked(enable);
590 }
591