Kstars

addcatalogobject.cpp
1/*
2 SPDX-FileCopyrightText: 2021 Valentin Boettcher <hiro at protagon.space; @hiro98:tchncs.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "addcatalogobject.h"
8#include "ui_addcatalogobject.h"
9#include "kstars_debug.h"
10
11#include <QInputDialog>
12#include <QPushButton>
13
15 : QDialog(parent), ui(new Ui::AddCatalogObject), m_object{ obj }
16{
17 ui->setupUi(this);
18 ui->ra->setUnits(dmsBox::HOURS);
19 fill_form_from_object();
20
22 [&](const auto &name) { m_object.setName(name); });
23
24 connect(ui->long_name, &QLineEdit::textChanged,
25 [&](const auto &name) { m_object.setLongName(name); });
26
27 connect(ui->catalog_identifier, &QLineEdit::textChanged,
28 [&](const auto &ident) { m_object.setCatalogIdentifier(ident); });
29
31 [&](const auto index) {
32 if (index > SkyObject::TYPE_UNKNOWN)
33 m_object.setType(SkyObject::TYPE_UNKNOWN);
34
35 m_object.setType(index);
36
37 refresh_flux();
38 });
39
40 auto validateAndStoreCoordinates = [&]() {
41 bool raOk(false), decOk(false);
42 auto ra = ui->ra->createDms(&raOk);
43 auto dec = ui->dec->createDms(&decOk);
44 auto* okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
45 Q_ASSERT(!!okButton);
46 okButton->setEnabled(raOk && decOk);
47 if (raOk && decOk) {
48 m_object.setRA0(ra);
49 m_object.setDec0(dec);
50 }
51 };
52
55
56 auto updateMag = [&]()
57 {
58 m_object.setMag(
59 ui->magUnknown->isChecked() ? NaN::f : static_cast<float>(ui->mag->value()));
60 };
62 connect(ui->magUnknown, &QCheckBox::stateChanged, updateMag);
63
65 [&](const auto value) { m_object.setMaj(static_cast<float>(value)); });
66
68 [&](const auto value) { m_object.setMin(static_cast<float>(value)); });
69
71 [&](const auto value) { m_object.setFlux(static_cast<float>(value)); });
72
74 [&](const auto value) { m_object.setPA(value); });
75
76 connect(ui->guessFromTextButton, &QPushButton::clicked, [this]() { this->guess_form_contents_from_text(); });
77}
78
79AddCatalogObject::~AddCatalogObject()
80{
81 delete ui;
82}
83
84void AddCatalogObject::fill_form_from_object()
85{
86 if (m_object.hasName()) // N.B. Avoid filling name fields with "unnamed"
87 ui->name->setText(m_object.name());
88 if (m_object.hasLongName())
89 ui->long_name->setText(m_object.longname());
90 ui->catalog_identifier->setText(m_object.catalogIdentifier());
91
92 for (int k = 0; k < SkyObject::NUMBER_OF_KNOWN_TYPES; ++k)
93 {
94 ui->type->addItem(SkyObject::typeName(k));
95 }
96 ui->type->addItem(SkyObject::typeName(SkyObject::TYPE_UNKNOWN));
97 ui->type->setCurrentIndex((int)(m_object.type()));
98
99 dms ra0 = m_object.ra0();
100 dms dec0 = m_object.dec0(); // Make a copy to avoid overwriting by signal-slot connection
101 ui->ra->show(ra0);
102 ui->dec->show(dec0);
103 if (std::isnan(m_object.mag()))
104 {
105 ui->magUnknown->setChecked(true);
106 }
107 else
108 {
109 ui->mag->setValue(m_object.mag());
110 ui->magUnknown->setChecked(false);
111 }
112 ui->flux->setValue(m_object.flux());
113 ui->position_angle->setValue(m_object.pa());
114 ui->maj->setValue(m_object.a());
115 ui->min->setValue(m_object.b());
116 refresh_flux();
117}
118
119void AddCatalogObject::refresh_flux()
120{
121 ui->flux->setEnabled(m_object.type() == SkyObject::RADIO_SOURCE);
122
123 if (!ui->flux->isEnabled())
124 ui->flux->setValue(0);
125}
126
127void AddCatalogObject::guess_form_contents_from_text()
128{
129 bool accepted = false;
131 this,
132 i18n("Guess object data from text"),
133 i18n("Copy-paste a text blurb with data on the object, and KStars will try to guess the contents of the fields from the text. The result is just a guess, so please verify the coordinates and the other information."),
134 QString(),
135 &accepted);
136 if (accepted && !text.isEmpty())
137 {
138 guess_form_contents_from_text(text);
139 }
140}
141
142void AddCatalogObject::guess_form_contents_from_text(QString text)
143{
144 // Parse text to fill in the entries in the form, using regexes to
145 // guess the field values
146
147 // TODO: Guess type from catalog name if the type match failed
148
149 QRegularExpression matchJ2000Line("^(.*)(?:J2000|ICRS|FK5|\\(2000(?:\\.0)?\\))(.*)$");
151 QRegularExpression matchCoords("(?:^|[^-\\d])([-+]?\\d\\d?)(?:h ?|d ?|[^\\d]?° ?|:| )(\\d\\d)(?:m ?|\' ?|’ ?|′ "
152 "?|:| )(\\d\\d(?:\\.\\d+)?)?(?:s|\"|\'\'|”|″)?\\b");
153 QRegularExpression matchCoords2("J?\\d{6,6}[-+]\\d{6,6}");
155 "(?:[mM]ag(?:nitudes?)?\\s*(?:\\([vV]\\))?|V(?=\\b))(?:\\s*=|:)?\\s*(-?\\d{1,2}(?:\\.\\d{1,3})?)");
156 QRegularExpression findMag2("\\b(-?\\d{1,2}\\.\\d{1,3})?\\s*(?:[mM]ag|V|B)\\b");
157 QRegularExpression findSize1("\\b(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(°|\'|\"|\'\')?\\s*[xX×]\\s*(\\d{1,3}(?:\\.\\d{1,2})"
158 "?)\\s*(°|\'|\"|\'\')?\\b");
159 QRegularExpression findSize2("\\b(?:[Ss]ize|[Dd]imensions?|[Dd]iameter)[: "
160 "](?:\\([vV]\\))?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(°|\'|\"|\'\')?\\b");
161 QRegularExpression findMajorAxis("\\b[Mm]ajor\\s*[Aa]xis:?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(°|\'|\"|\'\')?\\b");
162 QRegularExpression findMinorAxis("\\b[Mm]inor\\s*[Aa]xis:?\\s*(\\d{1,3}(?:\\.\\d{1,2})?)\\s*(°|\'|\"|\'\')?\\b");
163 QRegularExpression findPA(
164 "\\b(?:[Pp]osition *[Aa]ngle|PA|[pP]\\.[aA]\\.):?\\s*(\\d{1,3}(\\.\\d{1,2})?)(?:°|[Ddeg])?\\b");
166 "\\b(?:(?:[nN]ames?|NAMES?)[: ]|[iI]dent(?:ifier)?:|[dD]esignation:)\\h*\"?([-+\'A-Za-z0-9 ]*)\"?\\b");
168 catalogNames << "NGC"
169 << "IC"
170 << "M"
171 << "PGC"
172 << "UGC"
173 << "UGCA"
174 << "MCG"
175 << "ESO"
176 << "SDSS"
177 << "LEDA"
178 << "IRAS"
179 << "PNG"
180 << "Abell"
181 << "ACO"
182 << "HCG"
183 << "CGCG"
184 << "[IV]+ ?Zw"
185 << "Hickson"
186 << "AGC"
187 << "2MASS"
188 << "RCS2"
189 << "Terzan"
190 << "PN [A-Z0-9]"
191 << "VV"
192 << "PK"
193 << "GSC2"
194 << "LBN"
195 << "LDN"
196 << "Caldwell"
197 << "HIP"
198 << "AM"
199 << "vdB"
200 << "Barnard"
201 << "Shkh?"
202 << "KTG"
203 << "Palomar"
204 << "KPG"
205 << "CGPG"
206 << "TYC"
207 << "Arp"
208 << "Minkowski"
209 << "KUG"
210 << "DDO"
211 ;
213 objectTypes.append({"[Op]pen ?[Cc]luster|OCL|\\*Cl|Cl\\*|OpC|Cl\\?|Open Cl\\.", SkyObject::OPEN_CLUSTER});
214 objectTypes.append({"Globular ?Cluster|GlC|GCl|Glob\\. Cl\\.|Globular|Gl\\?", SkyObject::GLOBULAR_CLUSTER});
215 objectTypes.append({"(?:[Gg]as(?:eous)?|Diff(?:\\.|use)?|Emission|Ref(?:\\.|lection)?) ?[Nn]eb(?:ula|\\.)?|Neb|RfN|HII|GNe", SkyObject::GASEOUS_NEBULA});
216 objectTypes.append({"PN|PNe|Pl\\.? ?[Nn]eb\\.?|(?:[Pp]re[- ]?)[Pp]lanetary|PPNe|PPN|pA\\*|post[- ]?AGB", SkyObject::PLANETARY_NEBULA});
217 objectTypes.append({"SNR|[Ss]upernova ?[Rr]em(?:\\.|nant)?|SNRem(?:\\.|nant)?", SkyObject::SUPERNOVA_REMNANT});
218 objectTypes.append({"Gxy?|HIIG|H2G|[bB]CG|SBG|AGN|EmG|LINER|LIRG|GiG|GinP", SkyObject::GALAXY});
219 objectTypes.append({"Ast\\.?|Asterism", SkyObject::ASTERISM});
220 objectTypes.append({"ClG|GrG|CGG|[Gg](?:ala)?xy ?(?:[Gg]roup|[Cc]luster|[Pp]air|[Tt]rio|[Tt]riple)|GClus|GGrp|GGroup|GClstr|GPair|GTrpl|GTrio", SkyObject::GALAXY_CLUSTER});
221 objectTypes.append({"ISM|DNeb?|[dD](?:k\\.?|ark) ?[Nn]eb(?:ula|\\.)?", SkyObject::DARK_NEBULA});
222 objectTypes.append({"QSO|[qQ]uasar", SkyObject::QUASAR});
223 objectTypes.append({"(?:[Dd]ouble|[Dd]bl\\.?|[Tt]riple|[Mm]ult(?:iple|\\.)? ?[Ss]tar)|\\*\\*|Mult\\.? ?\\*", SkyObject::MULT_STAR});
224 objectTypes.append({"[nN]ebula", SkyObject::GASEOUS_NEBULA}); // Catch all nebula
225 objectTypes.append({"[gG]alaxy", SkyObject::GALAXY}); // Catch all galaxy
226
227 QRegularExpression findName2("\\b(" + catalogNames.join("|") + ")\\s+(J?[-+0-9\\.]+[A-Da-h]?)\\b");
228 QRegularExpression findName3("\\b([A-Za-z]+[0-9]?)\\s+(J?[-+0-9]+[A-Da-h]?)\\b");
229
230 // FIXME: This code will clean up by a lot if std::optional<> can be used
232 bool coordsFound = false,
233 magFound = false,
234 sizeFound = false,
235 nameFound = false,
236 positionAngleFound = false,
237 catalogDetermined = false,
238 typeFound = false;
239
240 dms RA, Dec;
241 float mag = NaN::f;
242 float majorAxis = NaN::f;
243 float minorAxis = NaN::f;
244 float positionAngle = 0;
247 QString catalogIdentifier;
248 SkyObject::TYPE type = SkyObject::TYPE_UNKNOWN;
249
250 // Note: The following method is a proxy to support older versions of Qt.
251 // In Qt 5.5 and above, the QString::indexOf(const QRegularExpression &re, int from, QRegularExpressionMatch *rmatch) method obviates the need for the following.
252 auto indexOf = [](const QString & s, const QRegularExpression & regExp, int from, QRegularExpressionMatch * m) -> int
253 {
254 *m = regExp.match(s, from);
255 return m->capturedStart(0);
256 };
257
258 auto countNonOverlappingMatches = [indexOf](const QString & string, const QRegularExpression & regExp,
259 QStringList *list = nullptr) -> int
260 {
261 int count = 0;
262 int matchIndex = -1;
263 int lastMatchLength = 1;
265 while ((matchIndex = indexOf(string, regExp, matchIndex + lastMatchLength, &rmatch)) >= 0)
266 {
267 ++count;
268 lastMatchLength = rmatch.captured(0).length();
269 if (list)
270 list->append(rmatch.captured(0));
271 }
272 return count;
273 };
274
277 std::size_t coordTextIndex = 0;
279 {
280 coordText = text;
281 }
282 else if (nonOverlappingMatchCount > 2)
283 {
284 qCDebug(KSTARS) << "Found more than 2 coordinate matches. Trying to match J2000 line.";
285 if ((coordTextIndex = indexOf(text, matchJ2000Line, 0, &rmatch)) >= 0)
286 {
287 coordText = rmatch.captured(1) + rmatch.captured(2);
288 qCDebug(KSTARS) << "Found a J2000 line match: " << coordText;
289 }
290 }
291
292 if (!coordText.isEmpty())
293 {
294 int coord1 = indexOf(coordText, matchCoords, 0, &rmatch);
295 if (coord1 >= 0) {
296 std::size_t length1 = rmatch.captured(0).length();
297 RA = dms(rmatch.captured(1) + ' ' + rmatch.captured(2) + ' ' + rmatch.captured(3), false);
298 int coord2 = indexOf(coordText, matchCoords, coord1 + length1, &rmatch);
299 if (coord2 >= 0) {
300 Dec = dms(rmatch.captured(1) + ' ' + rmatch.captured(2) + ' ' + rmatch.captured(3), true);
301 qCDebug(KSTARS) << "Extracted coordinates: " << RA.toHMSString() << " " << Dec.toDMSString();
302 coordsFound = true;
303
304 // Remove the coordinates from the original string so subsequent tasks don't confuse it
305 std::size_t length2 = rmatch.captured(0).length();
306 qCDebug(KSTARS) << "Eliminating text: " << text.midRef(coordTextIndex + coord1, length1) << " and " << text.midRef(coordTextIndex + coord2, length2);
307 text.replace(coordTextIndex + coord1, length1, "\n");
308 text.replace(coordTextIndex + coord2 - length1 + 1, length2, "\n");
309 qCDebug(KSTARS) << "Text now: " << text;
310 }
311 }
312 }
313 else
314 {
315 if (text.contains(matchCoords2, &rmatch))
316 {
317 QString matchString = rmatch.captured(0);
318 QRegularExpression extractCoords2("(\\d\\d)(\\d\\d)(\\d\\d)([-+]\\d\\d)(\\d\\d)(\\d\\d)");
320 RA = dms(rmatch.captured(1) + ' ' + rmatch.captured(2) + ' ' + rmatch.captured(3), false);
321 Dec = dms(rmatch.captured(4) + ' ' + rmatch.captured(5) + ' ' + rmatch.captured(6), true);
322 coordsFound = true;
323
324 // Remove coordinates to avoid downstream confusion with it
325 qCDebug(KSTARS) << "Eliminating text: " << text.midRef(rmatch.capturedStart(), rmatch.captured(0).length());
326 text.replace(rmatch.capturedStart(), rmatch.captured(0).length(), "\n");
327 qCDebug(KSTARS) << "Text now: " << text;
328 }
329 else
330 {
331 QStringList matches;
332 qCDebug(KSTARS) << "Could not extract RA/Dec. Found " << countNonOverlappingMatches(text, matchCoords, &matches)
333 << " coordinate matches:";
334 qCDebug(KSTARS) << matches;
335 }
336 }
337
338 // Type determination: Support full names of types, or SIMBAD/NED shorthands
339 for (const auto& p : objectTypes)
340 {
341 QRegularExpression findType("\\b(?:" + p.first + ")\\b");
342 if (text.contains(findType, &rmatch)) {
343 type = p.second;
344 typeFound = true;
345 qCDebug(KSTARS) << "Found Type: " << SkyObject::typeName(p.second);
346 qCDebug(KSTARS) << "Eliminating text: " << text.midRef(rmatch.capturedStart(), rmatch.captured(0).length());
347 text.replace(rmatch.capturedStart(), rmatch.captured(0).length(), "\n"); // Remove to avoid downstream confusion
348 qCDebug(KSTARS) << "Text now: " << text;
349 break;
350 }
351 }
352 if (!typeFound) {
353 qCDebug(KSTARS) << "Type not found";
354 }
355
356 nameFound = true;
357 catalogDetermined = true; // Transition to std::optional with C++17
358 if (text.contains(findName1, &rmatch)) // Explicit name search
359 {
360 qCDebug(KSTARS) << "Found explicit name field: " << rmatch.captured(1) << " in text " << rmatch.captured(0);
361 name = rmatch.captured(1);
362 catalogDetermined = false;
363 }
364 else if (text.contains(findName2, &rmatch))
365 {
366 catalogDetermined = true;
367 catalogName = rmatch.captured(1);
368 catalogIdentifier = rmatch.captured(2);
369 name = catalogName + ' ' + catalogIdentifier;
370 qCDebug(KSTARS) << "Found known catalog field: " << name
371 << " in text " << rmatch.captured(0);
372 }
373 else if (text.contains(findName3, &rmatch))
374 {
375 // N.B. This case is not strong enough to assume catalog name was found correctly
376 name = rmatch.captured(1) + ' ' + rmatch.captured(2);
377 qCDebug(KSTARS) << "Found something that looks like a catalog designation: "
378 << name << " in text " << rmatch.captured(0);
379 }
380 else
381 {
382 qCDebug(KSTARS) << "Could not find name.";
383 nameFound = false;
384 catalogDetermined = false;
385 }
386
387 magFound = true;
388 if (text.contains(findMag1, &rmatch))
389 {
390 qCDebug(KSTARS) << "Found magnitude: " << rmatch.captured(1) << " in text " << rmatch.captured(0);
391 mag = rmatch.captured(1).toFloat();
392 }
393 else if (text.contains(findMag2, &rmatch))
394 {
395 qCDebug(KSTARS) << "Found magnitude: " << rmatch.captured(1) << " in text " << rmatch.captured(0);
396 mag = rmatch.captured(1).toFloat();
397 }
398 else
399 {
400 qCDebug(KSTARS) << "Could not find magnitude.";
401 magFound = false;
402 }
403
404 sizeFound = true;
405 if (text.contains(findSize1, &rmatch))
406 {
407 qCDebug(KSTARS) << "Found size: " << rmatch.captured(1) << " x " << rmatch.captured(3) << " with units "
408 << rmatch.captured(4) << " in text " << rmatch.captured(0);
409 majorAxis = rmatch.captured(1).toFloat();
411 if (rmatch.captured(2).isEmpty())
412 {
413 unitText2 = rmatch.captured(4);
414 }
415 else
416 {
417 unitText2 = rmatch.captured(2);
418 }
419
420 if (unitText2.contains("°"))
421 majorAxis *= 60;
422 else if (unitText2.contains("\"") || unitText2.contains("\'\'"))
423 majorAxis /= 60;
424
425 minorAxis = rmatch.captured(3).toFloat();
426 if (rmatch.captured(4).contains("°"))
427 minorAxis *= 60;
428 else if (rmatch.captured(4).contains("\"") || rmatch.captured(4).contains("\'\'"))
429 minorAxis /= 60;
430 qCDebug(KSTARS) << "Major axis = " << majorAxis << "; minor axis = " << minorAxis << " in arcmin";
431 }
432 else if (text.contains(findSize2, &rmatch))
433 {
434 majorAxis = rmatch.captured(1).toFloat();
435 if (rmatch.captured(2).contains("°"))
436 majorAxis *= 60;
437 else if (rmatch.captured(2).contains("\"") || rmatch.captured(2).contains("\'\'"))
438 majorAxis /= 60;
440 }
441 else if (text.contains(findMajorAxis, &rmatch))
442 {
443 majorAxis = rmatch.captured(1).toFloat();
444 if (rmatch.captured(2).contains("°"))
445 majorAxis *= 60;
446 else if (rmatch.captured(2).contains("\"") || rmatch.captured(2).contains("\'\'"))
447 majorAxis /= 60;
449 if (text.contains(findMinorAxis, &rmatch))
450 {
451 minorAxis = rmatch.captured(1).toFloat();
452 if (rmatch.captured(2).contains("°"))
453 minorAxis *= 60;
454 else if (rmatch.captured(2).contains("\"") || rmatch.captured(2).contains("\'\'"))
455 minorAxis /= 60;
456 }
457 }
458
459 else
460 {
461 qCDebug(KSTARS)
462 << "Could not find size."; // FIXME: Improve to include separate major and minor axis matches, and size matches for round objects.
463 sizeFound = false;
464 }
465
466 positionAngleFound = true;
467 if (text.contains(findPA, &rmatch))
468 {
469 qCDebug(KSTARS) << "Found position angle: " << rmatch.captured(1) << " in text " << rmatch.captured(0);
470 positionAngle = rmatch.captured(1).toFloat();
471 }
472 else
473 {
474 qCDebug(KSTARS) << "Could not find position angle.";
475 positionAngleFound = false;
476 }
477
478 if (typeFound)
479 ui->type->setCurrentIndex((int)type);
480 if (nameFound)
481 ui->name->setText(name);
482 if (magFound)
483 {
484 ui->mag->setValue(mag);
485 ui->magUnknown->setChecked(false);
486 } else
487 {
488 ui->magUnknown->setChecked(true);
489 }
490 if (coordsFound)
491 {
492 ui->ra->show(RA);
493 ui->dec->show(Dec);
494 }
496 ui->position_angle->setValue(positionAngle);
497 if (sizeFound)
498 {
499 ui->maj->setValue(majorAxis);
500 ui->min->setValue(minorAxis);
501 }
503 {
504 ui->catalog_identifier->setText(catalogIdentifier);
505 }
506 refresh_flux();
507}
A simple data entry dialog to create and edit objects in CatalogDB catalogs.
AddCatalogObject(QWidget *parent, const CatalogObject &obj={})
A simple container object to hold the minimum information for a Deep Sky Object to be drawn on the sk...
float flux() const
float a() const
const QString & catalogIdentifier() const
void setMag(const double mag)
Set the magnitude of the object.
double pa() const override
float b() const
virtual QString name(void) const
Definition skyobject.h:145
virtual QString longname(void) const
Definition skyobject.h:164
int type(void) const
Definition skyobject.h:188
QString typeName() const
float mag() const
Definition skyobject.h:206
TYPE
The type classification of the SkyObject.
Definition skyobject.h:112
const CachingDms & ra0() const
Definition skypoint.h:251
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
const QString toDMSString(const bool forceSign=false, const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:287
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString name(StandardShortcut id)
void clicked(bool checked)
void stateChanged(int state)
void currentIndexChanged(int index)
void accepted()
void valueChanged(double d)
QString getMultiLineText(QWidget *parent, const QString &title, const QString &label, const QString &text, bool *ok, Qt::WindowFlags flags, Qt::InputMethodHints inputMethodHints)
void textChanged(const QString &text)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:02 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.