File indexing completed on 2024-04-14 03:43:22

0001 /*
0002     SPDX-FileCopyrightText: 2014 Akarsh Simha <akarsh.simha@kdemail.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 /* Project Includes */
0008 #include "nameresolver.h"
0009 #include "catalogobject.h"
0010 
0011 /* KDE Includes */
0012 #ifndef KSTARS_LITE
0013 #include <kio/filecopyjob.h>
0014 #else
0015 #include "kstarslite.h"
0016 #endif
0017 
0018 /* Qt Includes */
0019 #include <QUrl>
0020 #include <QTemporaryFile>
0021 #include <QString>
0022 #include <QXmlStreamReader>
0023 #include <QNetworkRequest>
0024 #include <QNetworkReply>
0025 #include <QNetworkAccessManager>
0026 #include <QEventLoop>
0027 
0028 #include <kstars_debug.h>
0029 
0030 std::pair<bool, CatalogObject> NameResolver::resolveName(const QString &name)
0031 {
0032     const auto &found_sesame{ NameResolverInternals::sesameResolver(name) };
0033     if (!found_sesame.first)
0034     {
0035         QString msg =
0036             xi18n("Error: sesameResolver failed. Could not resolve name on CDS Sesame.");
0037         qCDebug(KSTARS) << msg;
0038 
0039 #ifdef KSTARS_LITE
0040         KStarsLite::Instance()->notificationMessage(msg);
0041 #endif
0042     }
0043     // More to be done here if the resolved name is SIMBAD
0044     return found_sesame;
0045 }
0046 
0047 std::pair<bool, CatalogObject>
0048 NameResolver::NameResolverInternals::sesameResolver(const QString &name)
0049 {
0050     QUrl resolverUrl = QUrl(
0051         QString("http://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame/-oxpFI/SNV?%1").arg(name));
0052 
0053     QString msg = xi18n("Attempting to resolve object %1 using CDS Sesame.", name);
0054     qCDebug(KSTARS) << msg;
0055 
0056 #ifdef KSTARS_LITE
0057     KStarsLite::Instance()->notificationMessage(msg);
0058 #endif
0059 
0060     QNetworkAccessManager manager;
0061     QNetworkReply *response = manager.get(QNetworkRequest(resolverUrl));
0062     Q_ASSERT(response);
0063 
0064     // Wait synchronously
0065     QEventLoop event;
0066     QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
0067     event.exec();
0068 
0069     if (response->error() != QNetworkReply::NoError)
0070     {
0071         msg = xi18n("Error trying to get XML response from CDS Sesame server: %1",
0072                     response->errorString());
0073         qWarning() << msg;
0074 
0075 #ifdef KSTARS_LITE
0076         KStarsLite::Instance()->notificationMessage(msg);
0077 #endif
0078         return { false, {} };
0079     }
0080 
0081     QXmlStreamReader xml(response->readAll());
0082     response->deleteLater();
0083     if (xml.atEnd())
0084     {
0085         // file is empty
0086         msg = xi18n("Empty result instead of expected XML from CDS Sesame. Maybe bad "
0087                     "Internet connection?");
0088         qCDebug(KSTARS) << msg;
0089 
0090 #ifdef KSTARS_LITE
0091         KStarsLite::Instance()->notificationMessage(msg);
0092 #endif
0093         return { false, {} };
0094     }
0095 
0096     CatalogObject data{
0097         CatalogObject::oid{}, SkyObject::STAR, dms{ 0 }, dms{ 0 }, 0, name, name
0098     };
0099 
0100     bool found{ false };
0101     while (!xml.atEnd() && !xml.hasError())
0102     {
0103         QXmlStreamReader::TokenType token = xml.readNext();
0104 
0105         if (xml.isStartDocument())
0106             continue;
0107 
0108         if (token == QXmlStreamReader::StartElement)
0109         {
0110             qCDebug(KSTARS) << "Parsing token with name " << xml.name();
0111 
0112             if (xml.name() == "Resolver")
0113             {
0114                 found = true;
0115                 // This is the section we want
0116                 char resolver                   = 0;
0117                 QXmlStreamAttributes attributes = xml.attributes();
0118                 if (attributes.hasAttribute("name"))
0119                     resolver =
0120                         attributes.value("name")
0121                             .at(0)
0122                             .toLatin1(); // Expected to be S (Simbad), V (VizieR), or N (NED)
0123                 else
0124                 {
0125                     resolver = 0; // NUL character for unknown resolver
0126                     qWarning() << "Warning: Unknown resolver "
0127                                << attributes.value("name ")
0128                                << " while reading output from CDS Sesame";
0129                 }
0130 
0131                 qCDebug(KSTARS)
0132                     << "Resolved by " << resolver << attributes.value("name") << "!";
0133 
0134                 // Start reading the data to pick out the relevant ones
0135                 while (xml.readNextStartElement())
0136                 {
0137                     if (xml.name() == "otype")
0138                     {
0139                         const QString typeString = xml.readElementText();
0140                         data.setType(interpretObjectType(typeString));
0141                     }
0142                     else if (xml.name() == "jradeg")
0143                     {
0144                         data.setRA0(dms{ xml.readElementText().toDouble() });
0145                     }
0146                     else if (xml.name() == "jdedeg")
0147                     {
0148                         data.setDec0(dms{ xml.readElementText().toDouble() });
0149                     }
0150                     else if (xml.name() == "mag")
0151                     {
0152                         attributes = xml.attributes();
0153                         char band;
0154                         if (attributes.hasAttribute("band"))
0155                         {
0156                             band = attributes.value("band").at(0).toLatin1();
0157                         }
0158                         else
0159                         {
0160                             qWarning() << "Warning: Magnitude of unknown band found "
0161                                           "while reading output from CDS Sesame";
0162                             band = 0;
0163                         }
0164 
0165                         float mag = NaN::f;
0166                         xml.readNext();
0167                         if (xml.isCharacters())
0168                         {
0169                             qCDebug(KSTARS) << "characters: " << xml.tokenString();
0170                             mag = xml.tokenString().toFloat();
0171                         }
0172                         else if (xml.isStartElement())
0173                         {
0174                             while (xml.name() != "v")
0175                             {
0176                                 qCDebug(KSTARS) << "element: " << xml.name();
0177                                 xml.readNextStartElement();
0178                             }
0179                             mag = xml.readElementText().toFloat();
0180                             qCDebug(KSTARS)
0181                                 << "Got " << xml.tokenString() << " mag = " << mag;
0182                             while (!xml.atEnd() && xml.readNext() && xml.name() != "mag")
0183                                 ; // finish reading the <mag> tag all the way to </mag>
0184                         }
0185                         else
0186                             qWarning()
0187                                 << "Failed to parse Xml token in magnitude element: "
0188                                 << xml.tokenString();
0189 
0190                         if (band == 'V')
0191                         {
0192                             data.setMag(mag);
0193                         }
0194                         else if (band == 'B')
0195                         {
0196                             data.setFlux(mag); // FIXME: This is bad
0197                             if (std::isnan(data.mag()))
0198                                 data.setMag(mag); // FIXME: This is bad too
0199                         }
0200                         // Don't know what to do with other magnitudes, until we have a magnitude hash
0201                     }
0202                     else if (xml.name() == "oname") // Primary identifier
0203                     {
0204                         QString contents = xml.readElementText();
0205                         data.setCatalogIdentifier(contents);
0206                     }
0207                     else
0208                         xml.skipCurrentElement();
0209                     // TODO: Parse aliases for common names
0210                 }
0211                 break;
0212             }
0213             else
0214                 continue;
0215         }
0216     }
0217     if (xml.hasError())
0218     {
0219         msg = xi18n("Error parsing XML from CDS Sesame: %1 on line %2 @ col = %3",
0220                     xml.errorString(), xml.lineNumber(), xml.columnNumber());
0221         qCDebug(KSTARS) << msg;
0222 
0223 #ifdef KSTARS_LITE
0224         KStarsLite::Instance()->notificationMessage(msg);
0225 #endif
0226         return { false, {} };
0227     }
0228 
0229     if (!found)
0230         return { false, {} };
0231 
0232     msg = xi18n("Resolved %1 successfully.", name);
0233     qCDebug(KSTARS) << msg;
0234 
0235 #ifdef KSTARS_LITE
0236     KStarsLite::Instance()->notificationMessage(msg);
0237 #endif
0238     qCDebug(KSTARS) << "Object type: " << SkyObject::typeName(data.type())
0239                     << "; Coordinates: " << data.ra0().Degrees() << ";"
0240                     << data.dec().Degrees();
0241     return { true, data };
0242 }
0243 
0244 // bool NameResolver::NameResolverInternals::getDataFromSimbad( class CatalogObject &data ) {
0245 //     // TODO: Implement
0246 //     // QUrl( QString( "http://simbad.u-strasbg.fr/simbad/sim-script?script=output%20console=off%20script=off%0Aformat%20object%20%22%25DIM%22%0A" ) + data.name );
0247 // }
0248 
0249 SkyObject::TYPE NameResolver::NameResolverInternals::interpretObjectType(const QString &typeString, bool caseSensitive)
0250 {
0251     // FIXME: Due to the quirks of Sesame (SIMBAD vs NED etc), it
0252     // might be very difficult to discern the type in all cases.  The
0253     // best way TODO things might be to first run the query with NED,
0254     // and if it is extragalactic, then trust NED and
0255     // accept. Otherwise, or if NED did not return a result, re-run
0256     // the query on SIMBAD and VizieR and use that result, if any.
0257 
0258     // See https://simbad.u-strasbg.fr/simbad/sim-display?data=otypes for Object Classification in SIMBAD
0259 
0260     // Highest likelihood is a galaxy of some form, so we process that first
0261     const QString &s       = typeString; // To make the code compact
0262     Qt::CaseSensitivity cs = (caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
0263 
0264     QStringList galaxyTypes;
0265     QStringList galaxyGroupTypes;
0266     QStringList openClusterTypes;
0267     QStringList radioSourceTypes;
0268     galaxyTypes << "G"
0269                 << "LIN"
0270                 << "AGN"
0271                 << "GiG"
0272                 << "GiC"
0273                 << "H2G"
0274                 << "BiC"
0275                 << "GiP"
0276                 << "HzG"
0277                 << "rG"
0278                 << "AG?"
0279                 << "EmG"
0280                 << "LSB"
0281                 << "SBG"
0282                 << "bCG"
0283                 << "SyG"
0284                 << "Sy1"
0285                 << "Sy2"
0286                 << "Gxy";
0287 
0288     galaxyGroupTypes << "GClstr"
0289                      << "GGroup"
0290                      << "GPair"
0291                      << "ClG"
0292                      << "CGG"
0293                      << "PaG"
0294                      << "IG"
0295                      << "GrG"
0296                      << "SCG"; // NOTE (FIXME?): Compact groups and pairs of galaxies ar treated like galaxy clusters
0297 
0298     openClusterTypes << "*Cl"
0299                      << "Cl*"
0300                      << "OpC"
0301                      << "As*"
0302                      << "St*"
0303                      << "OCl";
0304 
0305     radioSourceTypes << "Rad"
0306                      << "mR"
0307                      << "cm"
0308                      << "mm"
0309                      << "smm"
0310                      << "HI"
0311                      << "rB"
0312                      << "Mas";
0313 
0314     if (galaxyTypes.contains(s, cs))
0315         return SkyObject::GALAXY;
0316 
0317     if (galaxyGroupTypes.contains(s, cs))
0318         return SkyObject::GALAXY_CLUSTER;
0319 
0320     if (openClusterTypes.contains(s, cs))
0321         return SkyObject::OPEN_CLUSTER; // FIXME: NED doesn't distinguish between globular clusters and open clusters!!
0322 
0323     auto check = [typeString, cs](const QString &type) { return (!QString::compare(typeString, type, cs)); };
0324 
0325     if (check("GlC") || check("GlCl"))
0326         return SkyObject::GLOBULAR_CLUSTER;
0327     if (check("Neb") || check("HII") || check("HH")) // FIXME: The last one is Herbig-Haro object
0328         return SkyObject::GASEOUS_NEBULA;
0329     if (check("SNR")) // FIXME: Simbad returns "ISM" for Veil Nebula (Interstellar Medium??)
0330         return SkyObject::SUPERNOVA_REMNANT;
0331     if (check("PN") || check("PNeb") || check("PNe") || check("pA*")) // FIXME: The latter is actually Proto PN
0332         return SkyObject::PLANETARY_NEBULA;
0333     if (typeString == "*")
0334         return SkyObject::CATALOG_STAR;
0335     if (check("QSO"))
0336         return SkyObject::QUASAR;
0337     if (check("DN") || check("DNe") || check("DNeb") || check("glb")) // The latter is Bok globule
0338         return SkyObject::DARK_NEBULA;
0339     if (radioSourceTypes.contains(s, cs))
0340         return SkyObject::RADIO_SOURCE;
0341     if (typeString == "**")
0342         return SkyObject::MULT_STAR;
0343     if (typeString.contains('*') && !check("C?*"))
0344         return SkyObject::CATALOG_STAR;
0345 
0346     return SkyObject::TYPE_UNKNOWN;
0347     // FIXME: complete this method
0348 }