Kstars

mountmodel.cpp
1 /* Ekos Mount MOdel
2  SPDX-FileCopyrightText: 2018 Robert Lancaster
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5  */
6 
7 #include "mountmodel.h"
8 
9 #include "align.h"
10 #include "kstars.h"
11 #include "kstarsdata.h"
12 #include "flagcomponent.h"
13 #include "ksnotification.h"
14 
15 #include "skymap.h"
16 #include "starobject.h"
17 #include "skymapcomposite.h"
18 #include "skyobject.h"
19 #include "dialogs/finddialog.h"
20 #include "QProgressIndicator.h"
21 
22 #include <ekos_align_debug.h>
23 
24 #define AL_FORMAT_VERSION 1.0
25 
26 // Qt version calming
27 #include <qtendl.h>
28 
29 namespace Ekos
30 {
31 MountModel::MountModel(Align *parent) : QDialog(parent)
32 {
33  setupUi(this);
34 
35  m_AlignInstance = parent;
36 
37  setWindowTitle("Mount Model Tool");
39  alignTable->setColumnWidth(0, 70);
40  alignTable->setColumnWidth(1, 75);
41  alignTable->setColumnWidth(2, 130);
42  alignTable->setColumnWidth(3, 30);
43 
44  wizardAlignB->setIcon(
45  QIcon::fromTheme("tools-wizard"));
46  wizardAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
47 
48  clearAllAlignB->setIcon(
49  QIcon::fromTheme("application-exit"));
50  clearAllAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
51 
52  removeAlignB->setIcon(QIcon::fromTheme("list-remove"));
53  removeAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
54 
55  addAlignB->setIcon(QIcon::fromTheme("list-add"));
56  addAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
57 
58  findAlignB->setIcon(QIcon::fromTheme("edit-find"));
59  findAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
60 
61  alignTable->verticalHeader()->setDragDropOverwriteMode(false);
62  alignTable->verticalHeader()->setSectionsMovable(true);
63  alignTable->verticalHeader()->setDragEnabled(true);
64  alignTable->verticalHeader()->setDragDropMode(QAbstractItemView::InternalMove);
65  connect(alignTable->verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this,
66  SLOT(moveAlignPoint(int, int, int)));
67 
68  loadAlignB->setIcon(
69  QIcon::fromTheme("document-open"));
70  loadAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
71 
72  saveAlignB->setIcon(
73  QIcon::fromTheme("document-save"));
74  saveAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
75 
76  previewB->setIcon(QIcon::fromTheme("kstars_grid"));
77  previewB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
78  previewB->setCheckable(true);
79 
80  sortAlignB->setIcon(QIcon::fromTheme("svn-update"));
81  sortAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
82 
83  stopAlignB->setIcon(
84  QIcon::fromTheme("media-playback-stop"));
85  stopAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
86 
87  startAlignB->setIcon(
88  QIcon::fromTheme("media-playback-start"));
89  startAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
90 
91  connect(wizardAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotWizardAlignmentPoints);
92  connect(alignTypeBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
93  &Ekos::MountModel::alignTypeChanged);
94 
95  connect(starListBox, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), this,
96  &Ekos::MountModel::slotStarSelected);
97  connect(greekStarListBox, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
98  this,
99  &Ekos::MountModel::slotStarSelected);
100 
101  connect(loadAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotLoadAlignmentPoints);
102  connect(saveAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotSaveAlignmentPoints);
103  connect(clearAllAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotClearAllAlignPoints);
104  connect(removeAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotRemoveAlignPoint);
105  connect(addAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotAddAlignPoint);
106  connect(findAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotFindAlignObject);
107  connect(sortAlignB, &QPushButton::clicked, this, &Ekos::MountModel::slotSortAlignmentPoints);
108 
109  connect(previewB, &QPushButton::clicked, this, &Ekos::MountModel::togglePreviewAlignPoints);
110  connect(stopAlignB, &QPushButton::clicked, this, &Ekos::MountModel::resetAlignmentProcedure);
111  connect(startAlignB, &QPushButton::clicked, this, &Ekos::MountModel::startStopAlignmentProcedure);
112 
113  generateAlignStarList();
114 
115 }
116 
117 MountModel::~MountModel()
118 {
119 
120 }
121 
122 void MountModel::generateAlignStarList()
123 {
124  alignStars.clear();
125  starListBox->clear();
126  greekStarListBox->clear();
127 
128  KStarsData *data = KStarsData::Instance();
130  listStars.append(data->skyComposite()->objectLists(SkyObject::STAR));
131  for (int i = 0; i < listStars.size(); i++)
132  {
133  QPair<QString, const SkyObject *> pair = listStars.value(i);
134  const StarObject *star = dynamic_cast<const StarObject *>(pair.second);
135  if (star)
136  {
137  StarObject *alignStar = star->clone();
138  alignStar->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
139  alignStars.append(alignStar);
140  }
141  }
142 
143  QStringList boxNames;
144  QStringList greekBoxNames;
145 
146  for (int i = 0; i < alignStars.size(); i++)
147  {
148  const StarObject *star = alignStars.value(i);
149  if (star)
150  {
151  if (!isVisible(star))
152  {
153  alignStars.remove(i);
154  i--;
155  }
156  else
157  {
158  if (star->hasLatinName())
159  boxNames << star->name();
160  else
161  {
162  if (!star->gname().isEmpty())
163  greekBoxNames << star->gname().simplified();
164  }
165  }
166  }
167  }
168 
169  boxNames.sort(Qt::CaseInsensitive);
170  boxNames.removeDuplicates();
171  greekBoxNames.removeDuplicates();
172  std::sort(greekBoxNames.begin(), greekBoxNames.end(), [](const QString & a, const QString & b)
173  {
174  QStringList aParts = a.split(' ');
175  QStringList bParts = b.split(' ');
176  if (aParts.length() < 2 || bParts.length() < 2)
177  return a < b; //This should not happen, they should all have 2 words in the string.
178  if (aParts[1] == bParts[1])
179  {
180  return aParts[0] < bParts[0]; //This compares the greek letter when the constellation is the same
181  }
182  else
183  return aParts[1] < bParts[1]; //This compares the constellation names
184  });
185 
186  starListBox->addItem("Select one:");
187  greekStarListBox->addItem("Select one:");
188  for (int i = 0; i < boxNames.size(); i++)
189  starListBox->addItem(boxNames.at(i));
190  for (int i = 0; i < greekBoxNames.size(); i++)
191  greekStarListBox->addItem(greekBoxNames.at(i));
192 }
193 
194 bool MountModel::isVisible(const SkyObject *so)
195 {
196  return (getAltitude(so) > 30);
197 }
198 
199 double MountModel::getAltitude(const SkyObject *so)
200 {
201  KStarsData *data = KStarsData::Instance();
202  SkyPoint sp = so->recomputeCoords(data->ut(), data->geo());
203 
204  //check altitude of object at this time.
205  sp.EquatorialToHorizontal(data->lst(), data->geo()->lat());
206 
207  return sp.alt().Degrees();
208 }
209 
210 void MountModel::togglePreviewAlignPoints()
211 {
212  previewShowing = !previewShowing;
213  previewB->setChecked(previewShowing);
214  updatePreviewAlignPoints();
215 }
216 
217 void MountModel::updatePreviewAlignPoints()
218 {
219  FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags();
220  for (int i = 0; i < flags->size(); i++)
221  {
222  if (flags->label(i).startsWith(QLatin1String("Align")))
223  {
224  flags->remove(i);
225  i--;
226  }
227  }
228  if (previewShowing)
229  {
230  for (int i = 0; i < alignTable->rowCount(); i++)
231  {
232  QTableWidgetItem *raCell = alignTable->item(i, 0);
233  QTableWidgetItem *deCell = alignTable->item(i, 1);
234  QTableWidgetItem *objNameCell = alignTable->item(i, 2);
235 
236  if (raCell && deCell && objNameCell)
237  {
238  QString raString = raCell->text();
239  QString deString = deCell->text();
240  dms raDMS = dms::fromString(raString, false);
241  dms decDMS = dms::fromString(deString, true);
242 
243  QString objString = objNameCell->text();
244 
245  SkyPoint flagPoint(raDMS, decDMS);
246  flags->add(flagPoint, "J2000", "Default", "Align " + QString::number(i + 1) + ' ' + objString, "white");
247  }
248  }
249  }
250  KStars::Instance()->map()->forceUpdate(true);
251 }
252 
253 void MountModel::slotLoadAlignmentPoints()
254 {
255  QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18nc("@title:window", "Open Ekos Alignment List"),
256  alignURL,
257  "Ekos AlignmentList (*.eal)");
258  if (fileURL.isEmpty())
259  return;
260 
261  if (fileURL.isValid() == false)
262  {
263  QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
264  KSNotification::sorry(message, i18n("Invalid URL"));
265  return;
266  }
267 
268  alignURL = fileURL;
269 
270  loadAlignmentPoints(fileURL.toLocalFile());
271  if (previewShowing)
272  updatePreviewAlignPoints();
273 }
274 
275 bool MountModel::loadAlignmentPoints(const QString &fileURL)
276 {
277  QFile sFile;
278  sFile.setFileName(fileURL);
279 
280  if (!sFile.open(QIODevice::ReadOnly))
281  {
282  QString message = i18n("Unable to open file %1", fileURL);
283  KSNotification::sorry(message, i18n("Could Not Open File"));
284  return false;
285  }
286 
287  alignTable->setRowCount(0);
288 
289  LilXML *xmlParser = newLilXML();
290 
291  char errmsg[MAXRBUF];
292  XMLEle *root = nullptr;
293  char c;
294 
295  while (sFile.getChar(&c))
296  {
297  root = readXMLEle(xmlParser, c, errmsg);
298 
299  if (root)
300  {
301  double sqVersion = atof(findXMLAttValu(root, "version"));
302  if (sqVersion < AL_FORMAT_VERSION)
303  {
304  emit newLog(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.",
305  sqVersion));
306  return false;
307  }
308 
309  XMLEle *ep = nullptr;
310  XMLEle *subEP = nullptr;
311 
312  int currentRow = 0;
313 
314  for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
315  {
316  if (!strcmp(tagXMLEle(ep), "AlignmentPoint"))
317  {
318  alignTable->insertRow(currentRow);
319 
320  subEP = findXMLEle(ep, "RA");
321  if (subEP)
322  {
323  QTableWidgetItem *RAReport = new QTableWidgetItem();
324  RAReport->setText(pcdataXMLEle(subEP));
326  alignTable->setItem(currentRow, 0, RAReport);
327  }
328  else
329  return false;
330  subEP = findXMLEle(ep, "DE");
331  if (subEP)
332  {
333  QTableWidgetItem *DEReport = new QTableWidgetItem();
334  DEReport->setText(pcdataXMLEle(subEP));
336  alignTable->setItem(currentRow, 1, DEReport);
337  }
338  else
339  return false;
340  subEP = findXMLEle(ep, "NAME");
341  if (subEP)
342  {
343  QTableWidgetItem *ObjReport = new QTableWidgetItem();
344  ObjReport->setText(pcdataXMLEle(subEP));
346  alignTable->setItem(currentRow, 2, ObjReport);
347  }
348  else
349  return false;
350  }
351  currentRow++;
352  }
353  return true;
354  }
355  }
356  return false;
357 }
358 
359 void MountModel::slotSaveAlignmentPoints()
360 {
361  QUrl backupCurrent = alignURL;
362 
363  if (alignURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || alignURL.toLocalFile().contains("/Temp"))
364  alignURL.clear();
365 
366  alignURL = QFileDialog::getSaveFileUrl(this, i18nc("@title:window", "Save Ekos Alignment List"), alignURL,
367  "Ekos Alignment List (*.eal)");
368  // if user presses cancel
369  if (alignURL.isEmpty())
370  {
371  alignURL = backupCurrent;
372  return;
373  }
374 
375  if (alignURL.toLocalFile().endsWith(QLatin1String(".eal")) == false)
376  alignURL.setPath(alignURL.toLocalFile() + ".eal");
377 
378 
379  if (alignURL.isValid())
380  {
381  if ((saveAlignmentPoints(alignURL.toLocalFile())) == false)
382  {
383  KSNotification::error(i18n("Failed to save alignment list"), i18n("Save"));
384  return;
385  }
386  }
387  else
388  {
389  QString message = i18n("Invalid URL: %1", alignURL.url());
390  KSNotification::sorry(message, i18n("Invalid URL"));
391  }
392 }
393 
394 bool MountModel::saveAlignmentPoints(const QString &path)
395 {
396  QFile file;
397  file.setFileName(path);
398  if (!file.open(QIODevice::WriteOnly))
399  {
400  QString message = i18n("Unable to write to file %1", path);
401  KSNotification::sorry(message, i18n("Could Not Open File"));
402  return false;
403  }
404 
405  QTextStream outstream(&file);
406 
407  outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << Qt::endl;
408  outstream << "<AlignmentList version='" << AL_FORMAT_VERSION << "'>" << Qt::endl;
409 
410  for (int i = 0; i < alignTable->rowCount(); i++)
411  {
412  QTableWidgetItem *raCell = alignTable->item(i, 0);
413  QTableWidgetItem *deCell = alignTable->item(i, 1);
414  QTableWidgetItem *objNameCell = alignTable->item(i, 2);
415 
416  if (!raCell || !deCell || !objNameCell)
417  return false;
418  QString raString = raCell->text();
419  QString deString = deCell->text();
420  QString objString = objNameCell->text();
421 
422  outstream << "<AlignmentPoint>" << Qt::endl;
423  outstream << "<RA>" << raString << "</RA>" << Qt::endl;
424  outstream << "<DE>" << deString << "</DE>" << Qt::endl;
425  outstream << "<NAME>" << objString << "</NAME>" << Qt::endl;
426  outstream << "</AlignmentPoint>" << Qt::endl;
427  }
428  outstream << "</AlignmentList>" << Qt::endl;
429  emit newLog(i18n("Alignment List saved to %1", path));
430  file.close();
431  return true;
432 }
433 
434 void MountModel::slotSortAlignmentPoints()
435 {
436  int firstAlignmentPt = findClosestAlignmentPointToTelescope();
437  if (firstAlignmentPt != -1)
438  {
439  swapAlignPoints(firstAlignmentPt, 0);
440  }
441 
442  for (int i = 0; i < alignTable->rowCount() - 1; i++)
443  {
444  int nextAlignmentPoint = findNextAlignmentPointAfter(i);
445  if (nextAlignmentPoint != -1)
446  {
447  swapAlignPoints(nextAlignmentPoint, i + 1);
448  }
449  }
450  if (previewShowing)
451  updatePreviewAlignPoints();
452 }
453 
454 int MountModel::findClosestAlignmentPointToTelescope()
455 {
456  dms bestDiff = dms(360);
457  double index = -1;
458 
459  for (int i = 0; i < alignTable->rowCount(); i++)
460  {
461  QTableWidgetItem *raCell = alignTable->item(i, 0);
462  QTableWidgetItem *deCell = alignTable->item(i, 1);
463 
464  if (raCell && deCell)
465  {
466  dms raDMS = dms::fromString(raCell->text(), false);
467  dms deDMS = dms::fromString(deCell->text(), true);
468 
469  SkyPoint sk(raDMS, deDMS);
470  dms thisDiff = telescopeCoord.angularDistanceTo(&sk);
471  if (thisDiff.Degrees() < bestDiff.Degrees())
472  {
473  index = i;
474  bestDiff = thisDiff;
475  }
476  }
477  }
478  return index;
479 }
480 
481 int MountModel::findNextAlignmentPointAfter(int currentSpot)
482 {
483  QTableWidgetItem *currentRACell = alignTable->item(currentSpot, 0);
484  QTableWidgetItem *currentDECell = alignTable->item(currentSpot, 1);
485 
486  if (currentRACell && currentDECell)
487  {
488  dms thisRADMS = dms::fromString(currentRACell->text(), false);
489  dms thisDEDMS = dms::fromString(currentDECell->text(), true);
490 
491  SkyPoint thisPt(thisRADMS, thisDEDMS);
492 
493  dms bestDiff = dms(360);
494  double index = -1;
495 
496  for (int i = currentSpot + 1; i < alignTable->rowCount(); i++)
497  {
498  QTableWidgetItem *raCell = alignTable->item(i, 0);
499  QTableWidgetItem *deCell = alignTable->item(i, 1);
500 
501  if (raCell && deCell)
502  {
503  dms raDMS = dms::fromString(raCell->text(), false);
504  dms deDMS = dms::fromString(deCell->text(), true);
505  SkyPoint point(raDMS, deDMS);
506  dms thisDiff = thisPt.angularDistanceTo(&point);
507 
508  if (thisDiff.Degrees() < bestDiff.Degrees())
509  {
510  index = i;
511  bestDiff = thisDiff;
512  }
513  }
514  }
515  return index;
516  }
517  else
518  return -1;
519 }
520 
521 void MountModel::slotWizardAlignmentPoints()
522 {
523  int points = alignPtNum->value();
524  if (points <
525  2) //The minimum is 2 because the wizard calculations require the calculation of an angle between points.
526  return; //It should not be less than 2 because the minimum in the spin box is 2.
527 
528  int minAlt = minAltBox->value();
529  KStarsData *data = KStarsData::Instance();
530  GeoLocation *geo = data->geo();
531  double lat = geo->lat()->Degrees();
532 
533  if (alignTypeBox->currentIndex() == OBJECT_FIXED_DEC)
534  {
535  double decAngle = alignDec->value();
536  //Dec that never rises.
537  if (lat > 0)
538  {
539  if (decAngle < lat - 90 + minAlt) //Min altitude possible at minAlt deg above horizon
540  {
541  KSNotification::sorry(i18n("DEC is below the altitude limit"));
542  return;
543  }
544  }
545  else
546  {
547  if (decAngle > lat + 90 - minAlt) //Max altitude possible at minAlt deg above horizon
548  {
549  KSNotification::sorry(i18n("DEC is below the altitude limit"));
550  return;
551  }
552  }
553  }
554 
555  //If there are less than 6 points, keep them all in the same DEC,
556  //any more, set the num per row to be the sqrt of the points to evenly distribute in RA and DEC
557  int numRAperDEC = 5;
558  if (points > 5)
559  numRAperDEC = qSqrt(points);
560 
561  //These calculations rely on modulus and int division counting beginning at 0, but the #s start at 1.
562  int decPoints = (points - 1) / numRAperDEC + 1;
563  int lastSetRAPoints = (points - 1) % numRAperDEC + 1;
564 
565  double decIncrement = -1;
566  double initDEC = -1;
567  SkyPoint spTest;
568 
569  if (alignTypeBox->currentIndex() == OBJECT_FIXED_DEC)
570  {
571  decPoints = 1;
572  initDEC = alignDec->value();
573  decIncrement = 0;
574  }
575  else if (decPoints == 1)
576  {
577  decIncrement = 0;
578  spTest.setAlt(
579  minAlt); //The goal here is to get the point exactly West at the minAlt so that we can use that DEC
580  spTest.setAz(270);
582  initDEC = spTest.dec().Degrees();
583  }
584  else
585  {
586  spTest.setAlt(
587  minAlt +
588  10); //We don't want to be right at the minAlt because there would be only 1 point on the dec circle above the alt.
589  spTest.setAz(180);
591  initDEC = spTest.dec().Degrees();
592  if (lat > 0)
593  decIncrement = (80 - initDEC) / (decPoints); //Don't quite want to reach NCP
594  else
595  decIncrement = (initDEC - 80) / (decPoints); //Don't quite want to reach SCP
596  }
597 
598  for (int d = 0; d < decPoints; d++)
599  {
600  double initRA = -1;
601  double raPoints = -1;
602  double raIncrement = -1;
603  double dec;
604 
605  if (lat > 0)
606  dec = initDEC + d * decIncrement;
607  else
608  dec = initDEC - d * decIncrement;
609 
610  if (alignTypeBox->currentIndex() == OBJECT_FIXED_DEC)
611  {
612  raPoints = points;
613  }
614  else if (d == decPoints - 1)
615  {
616  raPoints = lastSetRAPoints;
617  }
618  else
619  {
620  raPoints = numRAperDEC;
621  }
622 
623  //This computes both the initRA and the raIncrement.
624  calculateAngleForRALine(raIncrement, initRA, dec, lat, raPoints, minAlt);
625 
626  if (raIncrement == -1 || decIncrement == -1)
627  {
628  KSNotification::sorry(i18n("Point calculation error."));
629  return;
630  }
631 
632  for (int i = 0; i < raPoints; i++)
633  {
634  double ra = initRA + i * raIncrement;
635 
636  const SkyObject *original = getWizardAlignObject(ra, dec);
637 
638  QString ra_report, dec_report, name;
639 
640  if (original)
641  {
642  SkyObject *o = original->clone();
643  o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
644  getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report);
645  name = o->longname();
646  }
647  else
648  {
649  getFormattedCoords(dms(ra).Hours(), dec, ra_report, dec_report);
650  name = i18n("Sky Point");
651  }
652 
653  int currentRow = alignTable->rowCount();
654  alignTable->insertRow(currentRow);
655 
656  QTableWidgetItem *RAReport = new QTableWidgetItem();
657  RAReport->setText(ra_report);
659  alignTable->setItem(currentRow, 0, RAReport);
660 
661  QTableWidgetItem *DECReport = new QTableWidgetItem();
662  DECReport->setText(dec_report);
664  alignTable->setItem(currentRow, 1, DECReport);
665 
666  QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
667  ObjNameReport->setText(name);
668  ObjNameReport->setTextAlignment(Qt::AlignHCenter);
669  alignTable->setItem(currentRow, 2, ObjNameReport);
670 
671  QTableWidgetItem *disabledBox = new QTableWidgetItem();
672  disabledBox->setFlags(Qt::ItemIsSelectable);
673  alignTable->setItem(currentRow, 3, disabledBox);
674  }
675  }
676  if (previewShowing)
677  updatePreviewAlignPoints();
678 }
679 
680 void MountModel::calculateAngleForRALine(double &raIncrement, double &initRA, double initDEC, double lat, double raPoints,
681  double minAlt)
682 {
683  SkyPoint spEast;
684  SkyPoint spWest;
685 
686  //Circumpolar dec
687  if (fabs(initDEC) > (90 - fabs(lat) + minAlt))
688  {
689  if (raPoints > 1)
690  raIncrement = 360 / (raPoints - 1);
691  else
692  raIncrement = 0;
693  initRA = 0;
694  }
695  else
696  {
697  dms AZEast, AZWest;
698  calculateAZPointsForDEC(dms(initDEC), dms(minAlt), AZEast, AZWest);
699 
700  spEast.setAlt(minAlt);
701  spEast.setAz(AZEast.Degrees());
702  spEast.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
703 
704  spWest.setAlt(minAlt);
705  spWest.setAz(AZWest.Degrees());
706  spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat());
707 
708  dms angleSep = spEast.ra().deltaAngle(spWest.ra());
709 
710  initRA = spWest.ra().Degrees();
711  if (raPoints > 1)
712  raIncrement = fabs(angleSep.Degrees() / (raPoints - 1));
713  else
714  raIncrement = 0;
715  }
716 }
717 
718 void MountModel::calculateAZPointsForDEC(dms dec, dms alt, dms &AZEast, dms &AZWest)
719 {
720  KStarsData *data = KStarsData::Instance();
721  GeoLocation *geo = data->geo();
722  double AZRad;
723 
724  double sindec, cosdec, sinlat, coslat;
725  double sinAlt, cosAlt;
726 
727  geo->lat()->SinCos(sinlat, coslat);
728  dec.SinCos(sindec, cosdec);
729  alt.SinCos(sinAlt, cosAlt);
730 
731  double arg = (sindec - sinlat * sinAlt) / (coslat * cosAlt);
732  AZRad = acos(arg);
733  AZEast.setRadians(AZRad);
734  AZWest.setRadians(2.0 * dms::PI - AZRad);
735 }
736 
737 const SkyObject *MountModel::getWizardAlignObject(double ra, double dec)
738 {
739  double maxSearch = 5.0;
740  switch (alignTypeBox->currentIndex())
741  {
742  case OBJECT_ANY_OBJECT:
743  return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch);
744  case OBJECT_FIXED_DEC:
745  case OBJECT_FIXED_GRID:
746  return nullptr;
747 
748  case OBJECT_ANY_STAR:
749  return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch);
750  }
751 
752  //If they want named stars, then try to search for and return the closest Align Star to the requested location
753 
754  dms bestDiff = dms(360);
755  double index = -1;
756  for (int i = 0; i < alignStars.size(); i++)
757  {
758  const StarObject *star = alignStars.value(i);
759  if (star)
760  {
761  if (star->hasName())
762  {
763  SkyPoint thisPt(ra / 15.0, dec);
764  dms thisDiff = thisPt.angularDistanceTo(star);
765  if (thisDiff.Degrees() < bestDiff.Degrees())
766  {
767  index = i;
768  bestDiff = thisDiff;
769  }
770  }
771  }
772  }
773  if (index == -1)
774  return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch);
775  return alignStars.value(index);
776 }
777 
778 void MountModel::alignTypeChanged(int alignType)
779 {
780  if (alignType == OBJECT_FIXED_DEC)
781  alignDec->setEnabled(true);
782  else
783  alignDec->setEnabled(false);
784 }
785 
786 void MountModel::slotStarSelected(const QString selectedStar)
787 {
788  for (int i = 0; i < alignStars.size(); i++)
789  {
790  const StarObject *star = alignStars.value(i);
791  if (star)
792  {
793  if (star->name() == selectedStar || star->gname().simplified() == selectedStar)
794  {
795  int currentRow = alignTable->rowCount();
796  alignTable->insertRow(currentRow);
797 
798  QString ra_report, dec_report;
799  getFormattedCoords(star->ra0().Hours(), star->dec0().Degrees(), ra_report, dec_report);
800 
801  QTableWidgetItem *RAReport = new QTableWidgetItem();
802  RAReport->setText(ra_report);
804  alignTable->setItem(currentRow, 0, RAReport);
805 
806  QTableWidgetItem *DECReport = new QTableWidgetItem();
807  DECReport->setText(dec_report);
809  alignTable->setItem(currentRow, 1, DECReport);
810 
811  QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
812  ObjNameReport->setText(star->longname());
813  ObjNameReport->setTextAlignment(Qt::AlignHCenter);
814  alignTable->setItem(currentRow, 2, ObjNameReport);
815 
816  QTableWidgetItem *disabledBox = new QTableWidgetItem();
817  disabledBox->setFlags(Qt::ItemIsSelectable);
818  alignTable->setItem(currentRow, 3, disabledBox);
819 
820  starListBox->setCurrentIndex(0);
821  greekStarListBox->setCurrentIndex(0);
822  return;
823  }
824  }
825  }
826  if (previewShowing)
827  updatePreviewAlignPoints();
828 }
829 
830 
831 void MountModel::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str)
832 {
833  dms ra_s, dec_s;
834  ra_s.setH(ra);
835  dec_s.setD(dec);
836 
837  ra_str = QString("%1:%2:%3")
838  .arg(ra_s.hour(), 2, 10, QChar('0'))
839  .arg(ra_s.minute(), 2, 10, QChar('0'))
840  .arg(ra_s.second(), 2, 10, QChar('0'));
841  if (dec_s.Degrees() < 0)
842  dec_str = QString("-%1:%2:%3")
843  .arg(abs(dec_s.degree()), 2, 10, QChar('0'))
844  .arg(abs(dec_s.arcmin()), 2, 10, QChar('0'))
845  .arg(dec_s.arcsec(), 2, 10, QChar('0'));
846  else
847  dec_str = QString("%1:%2:%3")
848  .arg(dec_s.degree(), 2, 10, QChar('0'))
849  .arg(dec_s.arcmin(), 2, 10, QChar('0'))
850  .arg(dec_s.arcsec(), 2, 10, QChar('0'));
851 }
852 
853 void MountModel::slotClearAllAlignPoints()
854 {
855  if (alignTable->rowCount() == 0)
856  return;
857 
858  if (KMessageBox::questionYesNo(this, i18n("Are you sure you want to clear all the alignment points?"),
859  i18n("Clear Align Points")) == KMessageBox::Yes)
860  alignTable->setRowCount(0);
861 
862  if (previewShowing)
863  updatePreviewAlignPoints();
864 }
865 
866 void MountModel::slotRemoveAlignPoint()
867 {
868  alignTable->removeRow(alignTable->currentRow());
869  if (previewShowing)
870  updatePreviewAlignPoints();
871 }
872 
873 void MountModel::moveAlignPoint(int logicalIndex, int oldVisualIndex, int newVisualIndex)
874 {
875  Q_UNUSED(logicalIndex)
876 
877  for (int i = 0; i < alignTable->columnCount(); i++)
878  {
879  QTableWidgetItem *oldItem = alignTable->takeItem(oldVisualIndex, i);
880  QTableWidgetItem *newItem = alignTable->takeItem(newVisualIndex, i);
881 
882  alignTable->setItem(newVisualIndex, i, oldItem);
883  alignTable->setItem(oldVisualIndex, i, newItem);
884  }
885  alignTable->verticalHeader()->blockSignals(true);
886  alignTable->verticalHeader()->moveSection(newVisualIndex, oldVisualIndex);
887  alignTable->verticalHeader()->blockSignals(false);
888 
889  if (previewShowing)
890  updatePreviewAlignPoints();
891 }
892 
893 void MountModel::swapAlignPoints(int firstPt, int secondPt)
894 {
895  for (int i = 0; i < alignTable->columnCount(); i++)
896  {
897  QTableWidgetItem *firstPtItem = alignTable->takeItem(firstPt, i);
898  QTableWidgetItem *secondPtItem = alignTable->takeItem(secondPt, i);
899 
900  alignTable->setItem(firstPt, i, secondPtItem);
901  alignTable->setItem(secondPt, i, firstPtItem);
902  }
903 }
904 
905 void MountModel::slotAddAlignPoint()
906 {
907  int currentRow = alignTable->rowCount();
908  alignTable->insertRow(currentRow);
909 
910  QTableWidgetItem *disabledBox = new QTableWidgetItem();
911  disabledBox->setFlags(Qt::ItemIsSelectable);
912  alignTable->setItem(currentRow, 3, disabledBox);
913 }
914 
915 void MountModel::slotFindAlignObject()
916 {
917  if (FindDialog::Instance()->execWithParent(this) == QDialog::Accepted)
918  {
919  SkyObject *object = FindDialog::Instance()->targetObject();
920  if (object != nullptr)
921  {
922  KStarsData * const data = KStarsData::Instance();
923 
924  SkyObject *o = object->clone();
925  o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
926  int currentRow = alignTable->rowCount();
927  alignTable->insertRow(currentRow);
928 
929  QString ra_report, dec_report;
930  getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report);
931 
932  QTableWidgetItem *RAReport = new QTableWidgetItem();
933  RAReport->setText(ra_report);
935  alignTable->setItem(currentRow, 0, RAReport);
936 
937  QTableWidgetItem *DECReport = new QTableWidgetItem();
938  DECReport->setText(dec_report);
940  alignTable->setItem(currentRow, 1, DECReport);
941 
942  QTableWidgetItem *ObjNameReport = new QTableWidgetItem();
943  ObjNameReport->setText(o->longname());
944  ObjNameReport->setTextAlignment(Qt::AlignHCenter);
945  alignTable->setItem(currentRow, 2, ObjNameReport);
946 
947  QTableWidgetItem *disabledBox = new QTableWidgetItem();
948  disabledBox->setFlags(Qt::ItemIsSelectable);
949  alignTable->setItem(currentRow, 3, disabledBox);
950  }
951  }
952  if (previewShowing)
953  updatePreviewAlignPoints();
954 }
955 
956 void MountModel::resetAlignmentProcedure()
957 {
958  alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget());
959  QTableWidgetItem *statusReport = new QTableWidgetItem();
960  statusReport->setFlags(Qt::ItemIsSelectable);
961  statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
962  alignTable->setItem(currentAlignmentPoint, 3, statusReport);
963 
964  emit newLog(i18n("The Mount Model Tool is Reset."));
965  startAlignB->setIcon(
966  QIcon::fromTheme("media-playback-start"));
967  m_IsRunning = false;
968  currentAlignmentPoint = 0;
969  emit aborted();
970 }
971 
972 bool MountModel::alignmentPointsAreBad()
973 {
974  for (int i = 0; i < alignTable->rowCount(); i++)
975  {
976  QTableWidgetItem *raCell = alignTable->item(i, 0);
977  if (!raCell)
978  return true;
979  QString raString = raCell->text();
980  if (dms().setFromString(raString, false) == false)
981  return true;
982 
983  QTableWidgetItem *decCell = alignTable->item(i, 1);
984  if (!decCell)
985  return true;
986  QString decString = decCell->text();
987  if (dms().setFromString(decString, true) == false)
988  return true;
989  }
990  return false;
991 }
992 
993 void MountModel::startStopAlignmentProcedure()
994 {
995  if (!m_IsRunning)
996  {
997  if (alignTable->rowCount() > 0)
998  {
999  if (alignmentPointsAreBad())
1000  {
1001  KSNotification::error(i18n("Please Check the Alignment Points."));
1002  return;
1003  }
1004  if (m_AlignInstance->currentGOTOMode() == Align::GOTO_NOTHING)
1005  {
1007  nullptr,
1008  i18n("In the Align Module, \"Nothing\" is Selected for the Solver Action. This means that the "
1009  "mount model tool will not sync/align your mount but will only report the pointing model "
1010  "errors. Do you wish to continue?"),
1011  i18n("Pointing Model Report Only?"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
1012  "nothing_selected_warning");
1013  if (r == KMessageBox::Cancel)
1014  return;
1015  }
1016  if (currentAlignmentPoint == 0)
1017  {
1018  for (int row = 0; row < alignTable->rowCount(); row++)
1019  {
1020  QTableWidgetItem *statusReport = new QTableWidgetItem();
1021  statusReport->setIcon(QIcon());
1022  alignTable->setItem(row, 3, statusReport);
1023  }
1024  }
1025  startAlignB->setIcon(
1026  QIcon::fromTheme("media-playback-pause"));
1027  m_IsRunning = true;
1028  emit newLog(i18n("The Mount Model Tool is Starting."));
1029  startAlignmentPoint();
1030  }
1031  }
1032  else
1033  {
1034  startAlignB->setIcon(
1035  QIcon::fromTheme("media-playback-start"));
1036  alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget());
1037  emit newLog(i18n("The Mount Model Tool is Paused."));
1038  emit aborted();
1039  m_IsRunning = false;
1040 
1041  QTableWidgetItem *statusReport = new QTableWidgetItem();
1042  statusReport->setFlags(Qt::ItemIsSelectable);
1043  statusReport->setIcon(QIcon(":/icons/AlignWarning.svg"));
1044  alignTable->setItem(currentAlignmentPoint, 3, statusReport);
1045  }
1046 }
1047 
1048 void MountModel::startAlignmentPoint()
1049 {
1050  if (m_IsRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < alignTable->rowCount())
1051  {
1052  QTableWidgetItem *raCell = alignTable->item(currentAlignmentPoint, 0);
1053  QString raString = raCell->text();
1054  dms raDMS = dms::fromString(raString, false);
1055  double raDeg = raDMS.Degrees();
1056 
1057  QTableWidgetItem *decCell = alignTable->item(currentAlignmentPoint, 1);
1058  QString decString = decCell->text();
1059  dms decDMS = dms::fromString(decString, true);
1060  double dec = decDMS.Degrees();
1061 
1062  QProgressIndicator *alignIndicator = new QProgressIndicator(this);
1063  alignTable->setCellWidget(currentAlignmentPoint, 3, alignIndicator);
1064  alignIndicator->startAnimation();
1065 
1066  const SkyObject *target = getWizardAlignObject(raDeg, dec);
1067  m_AlignInstance->setTarget(*target);
1068  m_AlignInstance->Slew();
1069  }
1070 }
1071 
1072 void MountModel::finishAlignmentPoint(bool solverSucceeded)
1073 {
1074  if (m_IsRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < alignTable->rowCount())
1075  {
1076  alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget());
1077  QTableWidgetItem *statusReport = new QTableWidgetItem();
1078  statusReport->setFlags(Qt::ItemIsSelectable);
1079  if (solverSucceeded)
1080  statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg"));
1081  else
1082  statusReport->setIcon(QIcon(":/icons/AlignFailure.svg"));
1083  alignTable->setItem(currentAlignmentPoint, 3, statusReport);
1084 
1085  currentAlignmentPoint++;
1086 
1087  if (currentAlignmentPoint < alignTable->rowCount())
1088  {
1089  startAlignmentPoint();
1090  }
1091  else
1092  {
1093  m_IsRunning = false;
1094  startAlignB->setIcon(
1095  QIcon::fromTheme("media-playback-start"));
1096  emit newLog(i18n("The Mount Model Tool is Finished."));
1097  currentAlignmentPoint = 0;
1098  }
1099  }
1100 }
1101 
1102 void MountModel::setAlignStatus(Ekos::AlignState state)
1103 {
1104  switch (state)
1105  {
1106  case ALIGN_COMPLETE:
1107  if (m_IsRunning)
1108  finishAlignmentPoint(true);
1109  break;
1110 
1111  case ALIGN_FAILED:
1112  if (m_IsRunning)
1113  finishAlignmentPoint(false);
1114  break;
1115  default:
1116  break;
1117  }
1118 }
1119 }
const dms & alt() const
Definition: skypoint.h:281
QUrl getOpenFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes)
AlignHCenter
void setAlt(dms alt)
Sets Alt, the Altitude.
Definition: skypoint.h:194
static constexpr double PI
PI is a const static member; it's public so that it can be used anywhere, as long as dms....
Definition: dms.h:385
QTextStream & endl(QTextStream &stream)
QString number(int n, int base)
QString name(void) const override
If star is unnamed return "star" otherwise return the name.
Definition: starobject.h:130
void setTextAlignment(int alignment)
virtual void setD(const double &x)
Sets floating-point value of angle, in degrees.
Definition: dms.h:179
CaseInsensitive
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
Ekos is an advanced Astrophotography tool for Linux. It is based on a modular extensible framework to...
Definition: align.cpp:69
virtual bool open(QIODevice::OpenMode mode) override
Stores dms coordinates for a point in the sky. for converting between coordinate systems.
Definition: skypoint.h:44
int degree() const
Definition: dms.h:116
void remove(int index)
Remove a flag.
void append(const T &value)
void clicked(bool checked)
void SinCos(double &s, double &c) const
Compute Sine and Cosine of the angle simultaneously.
Definition: dms.h:444
QIcon fromTheme(const QString &name)
CachingDms * lst()
Definition: kstarsdata.h:224
SkyMap * map() const
Definition: kstars.h:141
int size()
Return the numbers of flags.
@ ALIGN_COMPLETE
Alignment successfully completed.
Definition: ekos.h:147
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition: skypoint.cpp:77
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString simplified() const const
int arcmin() const
Definition: dms.cpp:180
virtual SkyObject * clone() const
Create copy of object.
Definition: skyobject.cpp:50
static KStars * Instance()
Definition: kstars.h:123
The QProgressIndicator class lets an application display a progress indicator to show that a long tas...
KGuiItem cancel()
T value(int i) const const
bool isValid() const const
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition: dms.h:210
int size() const const
QString text() const const
void updateCoords(const KSNumbers *num, bool includePlanets=true, const CachingDms *lat=nullptr, const CachingDms *LST=nullptr, bool forceRecompute=false) override
Determine the current coordinates (RA, Dec) from the catalog coordinates (RA0, Dec0),...
Definition: starobject.cpp:258
QString gname(bool useGreekChars=true) const
Returns the genetive name of the star.
Definition: starobject.cpp:559
virtual void updateCoords(const KSNumbers *num, bool includePlanets=true, const CachingDms *lat=nullptr, const CachingDms *LST=nullptr, bool forceRecompute=false)
Determine the current coordinates (RA, Dec) from the catalog coordinates (RA0, Dec0),...
Definition: skypoint.cpp:582
ItemIsSelectable
QString i18n(const char *text, const TYPE &arg...)
QString longname(void) const override
If star is unnamed return "star" otherwise return the longname.
Definition: starobject.h:133
void setWindowFlags(Qt::WindowFlags type)
const CachingDms & dec() const
Definition: skypoint.h:269
bool isEmpty() const const
const CachingDms * lat() const
Definition: geolocation.h:70
void currentTextChanged(const QString &text)
subclass of SkyObject specialized for stars.
Definition: starobject.h:32
void startAnimation()
Starts the spin animation.
bool isEmpty() const const
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes)
GeoLocation * geo()
Definition: kstarsdata.h:230
void setText(const QString &text)
void add(const SkyPoint &flagPoint, QString epoch, QString image, QString label, QColor labelColor)
Add a flag.
SkyObject * objectNearest(SkyPoint *p, double &maxrad) override
void setWindowTitle(const QString &)
const T & at(int i) const const
void setFileName(const QString &name)
SkyPoint recomputeCoords(const KStarsDateTime &dt, const GeoLocation *geo=nullptr) const
The equatorial coordinates for the object on date dt are computed and returned, but the object's inte...
Definition: skyobject.cpp:295
QString toLocalFile() const const
QTextStream & dec(QTextStream &stream)
GeoCoordinates geo(const QVariant &location)
virtual void close() override
void setupUi(QWidget *widget)
int second() const
Definition: dms.cpp:231
bool hasLatinName() const
Definition: starobject.h:123
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
SkyMapComposite * skyComposite()
Definition: kstarsdata.h:166
An angle, stored as degrees, but expressible in many ways.
Definition: dms.h:37
@ ALIGN_FAILED
Alignment failed.
Definition: ekos.h:148
int removeDuplicates()
virtual void setRadians(const double &Rad)
Set angle according to the argument, in radians.
Definition: dms.h:333
const KStarsDateTime & ut() const
Definition: kstarsdata.h:157
void setAz(dms az)
Sets Az, the Azimuth.
Definition: skypoint.h:230
const CachingDms & ra() const
Definition: skypoint.h:263
void setFlags(Qt::ItemFlags flags)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
StarObject * clone() const override
Create copy of object.
Definition: starobject.cpp:126
const CachingDms & dec0() const
Definition: skypoint.h:257
const double & Degrees() const
Definition: dms.h:141
QString label(int index)
Get label.
void setPath(const QString &path, QUrl::ParsingMode mode)
QString name(StandardShortcut id)
bool hasName() const
Definition: starobject.h:120
void currentIndexChanged(int index)
const CachingDms & ra0() const
Definition: skypoint.h:251
int hour() const
Definition: dms.h:147
int arcsec() const
Definition: dms.cpp:193
const dms deltaAngle(dms angle) const
deltaAngle Return the shortest difference (path) between this angle and the supplied angle.
Definition: dms.cpp:259
KGuiItem cont()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
void HorizontalToEquatorial(const dms *LST, const dms *lat)
Determine the (RA, Dec) coordinates of the SkyPoint from its (Altitude, Azimuth) coordinates,...
Definition: skypoint.cpp:143
QList::iterator begin()
virtual QString longname(void) const
Definition: skyobject.h:164
int size() const const
KStarsData * data() const
Definition: kstars.h:135
void forceUpdate(bool now=false)
Recalculates the positions of objects in the sky, and then repaints the sky map.
Definition: skymap.cpp:1176
Represents a flag on the sky map. Each flag is composed by a SkyPoint where coordinates are stored,...
Definition: flagcomponent.h:33
int minute() const
Definition: dms.cpp:221
QList::iterator end()
AlignState
Definition: ekos.h:144
ButtonCode questionYesNo(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonYes=KStandardGuiItem::yes(), const KGuiItem &buttonNo=KStandardGuiItem::no(), const QString &dontAskAgainName=QString(), Options options=Notify)
void clear()
SkyObject * starNearest(SkyPoint *p, double &maxrad)
Information about an object in the sky.
Definition: skyobject.h:41
QObject * parent() const const
QString message
WA_LayoutUsesWidgetRect
double Hours() const
Definition: dms.h:168
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition: dms.cpp:421
Relevant data about an observing location on Earth.
Definition: geolocation.h:27
bool getChar(char *c)
void sort(Qt::CaseSensitivity cs)
void setIcon(const QIcon &icon)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Sep 26 2023 03:55:48 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.