diff --git a/kstars/auxiliary/ksutils.cpp b/kstars/auxiliary/ksutils.cpp index 8c130f914..5eb201c48 100644 --- a/kstars/auxiliary/ksutils.cpp +++ b/kstars/auxiliary/ksutils.cpp @@ -1,1473 +1,1645 @@ /*************************************************************************** ksutils.cpp - K Desktop Planetarium ------------------- begin : Mon Jan 7 10:48:09 EST 2002 copyright : (C) 2002 by Mark Hollomon email : mhh@mindspring.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "ksutils.h" #include "config-kstars.h" #include "ksnotification.h" +#include "ekos_align_debug.h" #include "deepskyobject.h" #ifndef KSTARS_LITE #include "kswizard.h" #endif #include "Options.h" #include "starobject.h" #include "auxiliary/kspaths.h" #ifndef KSTARS_LITE #include #endif #ifdef HAVE_LIBRAW #include #endif #include #include #include namespace KSUtils { bool isHardwareLimited() { #ifdef __arm__ return true; #else return false; #endif } bool openDataFile(QFile &file, const QString &s) { QString FileName = KSPaths::locate(QStandardPaths::GenericDataLocation, s); if (!FileName.isNull()) { file.setFileName(FileName); return file.open(QIODevice::ReadOnly); } return false; } QString getDSSURL(const SkyPoint *const p) { const DeepSkyObject *dso = nullptr; double height, width; double dss_default_size = Options::defaultDSSImageSize(); double dss_padding = Options::dSSPadding(); Q_ASSERT(p); Q_ASSERT(dss_default_size > 0.0 && dss_padding >= 0.0); dso = dynamic_cast(p); // Decide what to do about the height and width if (dso) { // For deep-sky objects, use their height and width information double a, b, pa; a = dso->a(); b = dso->a() * dso->e(); // Use a * e instead of b, since e() returns 1 whenever one of the dimensions is zero. This is important for circular objects pa = dso->pa() * dms::DegToRad; // We now want to convert a, b, and pa into an image // height and width -- i.e. a dRA and dDec. // DSS uses dDec for height and dRA for width. (i.e. "top" is north in the DSS images, AFAICT) // From some trigonometry, assuming we have a rectangular object (worst case), we need: width = a * sin(pa) + b * cos(pa); height = a * cos(pa) + b * sin(pa); // 'a' and 'b' are in arcminutes, so height and width are in arcminutes // Pad the RA and Dec, so that we show more of the sky than just the object. height += dss_padding; width += dss_padding; } else { // For a generic sky object, we don't know what to do. So // we just assume the default size. height = width = dss_default_size; } // There's no point in tiny DSS images that are smaller than dss_default_size if (height < dss_default_size) height = dss_default_size; if (width < dss_default_size) width = dss_default_size; return getDSSURL(p->ra0(), p->dec0(), width, height); } QString getDSSURL(const dms &ra, const dms &dec, float width, float height, const QString &type) { const QString URLprefix("http://archive.stsci.edu/cgi-bin/dss_search?"); QString URLsuffix = QString("&e=J2000&f=%1&c=none&fov=NONE").arg(type); const double dss_default_size = Options::defaultDSSImageSize(); char decsgn = (dec.Degrees() < 0.0) ? '-' : '+'; int dd = abs(dec.degree()); int dm = abs(dec.arcmin()); int ds = abs(dec.arcsec()); // Infinite and NaN sizes are replaced by the default size and tiny DSS images are resized to default size if (!qIsFinite(height) || height <= 0.0) height = dss_default_size; if (!qIsFinite(width) || width <= 0.0) width = dss_default_size; // DSS accepts images that are no larger than 75 arcminutes if (height > 75.0) height = 75.0; if (width > 75.0) width = 75.0; QString RAString, DecString, SizeString; DecString = DecString.sprintf("&d=%c%02d+%02d+%02d", decsgn, dd, dm, ds); RAString = RAString.sprintf("r=%02d+%02d+%02d", ra.hour(), ra.minute(), ra.second()); SizeString = SizeString.sprintf("&h=%02.1f&w=%02.1f", height, width); return (URLprefix + RAString + DecString + SizeString + URLsuffix); } QString toDirectionString(dms angle) { // TODO: Instead of doing it this way, it would be nicer to // compute the string to arbitrary precision. Although that will // not be easy to localize. (Consider, for instance, Indian // languages that have special names for the intercardinal points) // -- asimha static const char *directions[] = { I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "N"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "NNE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "NE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "ENE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "E"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "ESE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "SE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "SSE"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "S"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "SSW"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "SW"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "WSW"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "W"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "WNW"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "NW"), I18N_NOOP2("Abbreviated cardinal / intercardinal etc. direction", "NNW"), I18N_NOOP2("Unknown cardinal / intercardinal direction", "???") }; int index = (int)((angle.reduce().Degrees() + 11.25) / 22.5); // A number between 0 and 16 (inclusive) is expected if (index < 0 || index > 16) index = 16; // Something went wrong. else index = (index == 16 ? 0 : index); return i18nc("Abbreviated cardinal / intercardinal etc. direction", directions[index]); } QList *castStarObjListToSkyObjList(QList *starObjList) { QList *skyObjList = new QList(); foreach (StarObject *so, *starObjList) { skyObjList->append(so); } return skyObjList; } QString constGenetiveFromAbbrev(const QString &code) { if (code == "And") return QString("Andromedae"); if (code == "Ant") return QString("Antliae"); if (code == "Aps") return QString("Apodis"); if (code == "Aqr") return QString("Aquarii"); if (code == "Aql") return QString("Aquilae"); if (code == "Ara") return QString("Arae"); if (code == "Ari") return QString("Arietis"); if (code == "Aur") return QString("Aurigae"); if (code == "Boo") return QString("Bootis"); if (code == "Cae") return QString("Caeli"); if (code == "Cam") return QString("Camelopardalis"); if (code == "Cnc") return QString("Cancri"); if (code == "CVn") return QString("Canum Venaticorum"); if (code == "CMa") return QString("Canis Majoris"); if (code == "CMi") return QString("Canis Minoris"); if (code == "Cap") return QString("Capricorni"); if (code == "Car") return QString("Carinae"); if (code == "Cas") return QString("Cassiopeiae"); if (code == "Cen") return QString("Centauri"); if (code == "Cep") return QString("Cephei"); if (code == "Cet") return QString("Ceti"); if (code == "Cha") return QString("Chamaeleontis"); if (code == "Cir") return QString("Circini"); if (code == "Col") return QString("Columbae"); if (code == "Com") return QString("Comae Berenices"); if (code == "CrA") return QString("Coronae Austrinae"); if (code == "CrB") return QString("Coronae Borealis"); if (code == "Crv") return QString("Corvi"); if (code == "Crt") return QString("Crateris"); if (code == "Cru") return QString("Crucis"); if (code == "Cyg") return QString("Cygni"); if (code == "Del") return QString("Delphini"); if (code == "Dor") return QString("Doradus"); if (code == "Dra") return QString("Draconis"); if (code == "Equ") return QString("Equulei"); if (code == "Eri") return QString("Eridani"); if (code == "For") return QString("Fornacis"); if (code == "Gem") return QString("Geminorum"); if (code == "Gru") return QString("Gruis"); if (code == "Her") return QString("Herculis"); if (code == "Hor") return QString("Horologii"); if (code == "Hya") return QString("Hydrae"); if (code == "Hyi") return QString("Hydri"); if (code == "Ind") return QString("Indi"); if (code == "Lac") return QString("Lacertae"); if (code == "Leo") return QString("Leonis"); if (code == "LMi") return QString("Leonis Minoris"); if (code == "Lep") return QString("Leporis"); if (code == "Lib") return QString("Librae"); if (code == "Lup") return QString("Lupi"); if (code == "Lyn") return QString("Lyncis"); if (code == "Lyr") return QString("Lyrae"); if (code == "Men") return QString("Mensae"); if (code == "Mic") return QString("Microscopii"); if (code == "Mon") return QString("Monocerotis"); if (code == "Mus") return QString("Muscae"); if (code == "Nor") return QString("Normae"); if (code == "Oct") return QString("Octantis"); if (code == "Oph") return QString("Ophiuchi"); if (code == "Ori") return QString("Orionis"); if (code == "Pav") return QString("Pavonis"); if (code == "Peg") return QString("Pegasi"); if (code == "Per") return QString("Persei"); if (code == "Phe") return QString("Phoenicis"); if (code == "Pic") return QString("Pictoris"); if (code == "Psc") return QString("Piscium"); if (code == "PsA") return QString("Piscis Austrini"); if (code == "Pup") return QString("Puppis"); if (code == "Pyx") return QString("Pyxidis"); if (code == "Ret") return QString("Reticuli"); if (code == "Sge") return QString("Sagittae"); if (code == "Sgr") return QString("Sagittarii"); if (code == "Sco") return QString("Scorpii"); if (code == "Scl") return QString("Sculptoris"); if (code == "Sct") return QString("Scuti"); if (code == "Ser") return QString("Serpentis"); if (code == "Sex") return QString("Sextantis"); if (code == "Tau") return QString("Tauri"); if (code == "Tel") return QString("Telescopii"); if (code == "Tri") return QString("Trianguli"); if (code == "TrA") return QString("Trianguli Australis"); if (code == "Tuc") return QString("Tucanae"); if (code == "UMa") return QString("Ursae Majoris"); if (code == "UMi") return QString("Ursae Minoris"); if (code == "Vel") return QString("Velorum"); if (code == "Vir") return QString("Virginis"); if (code == "Vol") return QString("Volantis"); if (code == "Vul") return QString("Vulpeculae"); return code; } QString constNameFromAbbrev(const QString &code) { if (code == "And") return QString("Andromeda"); if (code == "Ant") return QString("Antlia"); if (code == "Aps") return QString("Apus"); if (code == "Aqr") return QString("Aquarius"); if (code == "Aql") return QString("Aquila"); if (code == "Ara") return QString("Ara"); if (code == "Ari") return QString("Aries"); if (code == "Aur") return QString("Auriga"); if (code == "Boo") return QString("Bootes"); if (code == "Cae") return QString("Caelum"); if (code == "Cam") return QString("Camelopardalis"); if (code == "Cnc") return QString("Cancer"); if (code == "CVn") return QString("Canes Venatici"); if (code == "CMa") return QString("Canis Major"); if (code == "CMi") return QString("Canis Minor"); if (code == "Cap") return QString("Capricornus"); if (code == "Car") return QString("Carina"); if (code == "Cas") return QString("Cassiopeia"); if (code == "Cen") return QString("Centaurus"); if (code == "Cep") return QString("Cepheus"); if (code == "Cet") return QString("Cetus"); if (code == "Cha") return QString("Chamaeleon"); if (code == "Cir") return QString("Circinus"); if (code == "Col") return QString("Columba"); if (code == "Com") return QString("Coma Berenices"); if (code == "CrA") return QString("Corona Australis"); if (code == "CrB") return QString("Corona Borealis"); if (code == "Crv") return QString("Corvus"); if (code == "Crt") return QString("Crater"); if (code == "Cru") return QString("Crux"); if (code == "Cyg") return QString("Cygnus"); if (code == "Del") return QString("Delphinus"); if (code == "Dor") return QString("Doradus"); if (code == "Dra") return QString("Draco"); if (code == "Equ") return QString("Equuleus"); if (code == "Eri") return QString("Eridanus"); if (code == "For") return QString("Fornax"); if (code == "Gem") return QString("Gemini"); if (code == "Gru") return QString("Grus"); if (code == "Her") return QString("Hercules"); if (code == "Hor") return QString("Horologium"); if (code == "Hya") return QString("Hydra"); if (code == "Hyi") return QString("Hydrus"); if (code == "Ind") return QString("Indus"); if (code == "Lac") return QString("Lacerta"); if (code == "Leo") return QString("Leo"); if (code == "LMi") return QString("Leo Minor"); if (code == "Lep") return QString("Lepus"); if (code == "Lib") return QString("Libra"); if (code == "Lup") return QString("Lupus"); if (code == "Lyn") return QString("Lynx"); if (code == "Lyr") return QString("Lyra"); if (code == "Men") return QString("Mensa"); if (code == "Mic") return QString("Microscopium"); if (code == "Mon") return QString("Monoceros"); if (code == "Mus") return QString("Musca"); if (code == "Nor") return QString("Norma"); if (code == "Oct") return QString("Octans"); if (code == "Oph") return QString("Ophiuchus"); if (code == "Ori") return QString("Orion"); if (code == "Pav") return QString("Pavo"); if (code == "Peg") return QString("Pegasus"); if (code == "Per") return QString("Perseus"); if (code == "Phe") return QString("Phoenix"); if (code == "Pic") return QString("Pictor"); if (code == "Psc") return QString("Pisces"); if (code == "PsA") return QString("Piscis Austrinus"); if (code == "Pup") return QString("Puppis"); if (code == "Pyx") return QString("Pyxis"); if (code == "Ret") return QString("Reticulum"); if (code == "Sge") return QString("Sagitta"); if (code == "Sgr") return QString("Sagittarius"); if (code == "Sco") return QString("Scorpius"); if (code == "Scl") return QString("Sculptor"); if (code == "Sct") return QString("Scutum"); if (code == "Ser") return QString("Serpens"); if (code == "Sex") return QString("Sextans"); if (code == "Tau") return QString("Taurus"); if (code == "Tel") return QString("Telescopium"); if (code == "Tri") return QString("Triangulum"); if (code == "TrA") return QString("Triangulum Australe"); if (code == "Tuc") return QString("Tucana"); if (code == "UMa") return QString("Ursa Major"); if (code == "UMi") return QString("Ursa Minor"); if (code == "Vel") return QString("Vela"); if (code == "Vir") return QString("Virgo"); if (code == "Vol") return QString("Volans"); if (code == "Vul") return QString("Vulpecula"); return code; } QString constNameToAbbrev(const QString &fullName_) { QString fullName = fullName_.toLower(); if (fullName == "andromeda") return QString("And"); if (fullName == "antlia") return QString("Ant"); if (fullName == "apus") return QString("Aps"); if (fullName == "aquarius") return QString("Aqr"); if (fullName == "aquila") return QString("Aql"); if (fullName == "ara") return QString("Ara"); if (fullName == "aries") return QString("Ari"); if (fullName == "auriga") return QString("Aur"); if (fullName == "bootes") return QString("Boo"); if (fullName == "caelum") return QString("Cae"); if (fullName == "camelopardalis") return QString("Cam"); if (fullName == "cancer") return QString("Cnc"); if (fullName == "canes venatici") return QString("CVn"); if (fullName == "canis major") return QString("CMa"); if (fullName == "canis minor") return QString("CMi"); if (fullName == "capricornus") return QString("Cap"); if (fullName == "carina") return QString("Car"); if (fullName == "cassiopeia") return QString("Cas"); if (fullName == "centaurus") return QString("Cen"); if (fullName == "cepheus") return QString("Cep"); if (fullName == "cetus") return QString("Cet"); if (fullName == "chamaeleon") return QString("Cha"); if (fullName == "circinus") return QString("Cir"); if (fullName == "columba") return QString("Col"); if (fullName == "coma berenices") return QString("Com"); if (fullName == "corona australis") return QString("CrA"); if (fullName == "corona borealis") return QString("CrB"); if (fullName == "corvus") return QString("Crv"); if (fullName == "crater") return QString("Crt"); if (fullName == "crux") return QString("Cru"); if (fullName == "cygnus") return QString("Cyg"); if (fullName == "delphinus") return QString("Del"); if (fullName == "doradus") return QString("Dor"); if (fullName == "draco") return QString("Dra"); if (fullName == "equuleus") return QString("Equ"); if (fullName == "eridanus") return QString("Eri"); if (fullName == "fornax") return QString("For"); if (fullName == "gemini") return QString("Gem"); if (fullName == "grus") return QString("Gru"); if (fullName == "hercules") return QString("Her"); if (fullName == "horologium") return QString("Hor"); if (fullName == "hydra") return QString("Hya"); if (fullName == "hydrus") return QString("Hyi"); if (fullName == "indus") return QString("Ind"); if (fullName == "lacerta") return QString("Lac"); if (fullName == "leo") return QString("Leo"); if (fullName == "leo minor") return QString("LMi"); if (fullName == "lepus") return QString("Lep"); if (fullName == "libra") return QString("Lib"); if (fullName == "lupus") return QString("Lup"); if (fullName == "lynx") return QString("Lyn"); if (fullName == "lyra") return QString("Lyr"); if (fullName == "mensa") return QString("Men"); if (fullName == "microscopium") return QString("Mic"); if (fullName == "monoceros") return QString("Mon"); if (fullName == "musca") return QString("Mus"); if (fullName == "norma") return QString("Nor"); if (fullName == "octans") return QString("Oct"); if (fullName == "ophiuchus") return QString("Oph"); if (fullName == "orion") return QString("Ori"); if (fullName == "pavo") return QString("Pav"); if (fullName == "pegasus") return QString("Peg"); if (fullName == "perseus") return QString("Per"); if (fullName == "phoenix") return QString("Phe"); if (fullName == "pictor") return QString("Pic"); if (fullName == "pisces") return QString("Psc"); if (fullName == "piscis austrinus") return QString("PsA"); if (fullName == "puppis") return QString("Pup"); if (fullName == "pyxis") return QString("Pyx"); if (fullName == "reticulum") return QString("Ret"); if (fullName == "sagitta") return QString("Sge"); if (fullName == "sagittarius") return QString("Sgr"); if (fullName == "scorpius") return QString("Sco"); if (fullName == "sculptor") return QString("Scl"); if (fullName == "scutum") return QString("Sct"); if (fullName == "serpens") return QString("Ser"); if (fullName == "sextans") return QString("Sex"); if (fullName == "taurus") return QString("Tau"); if (fullName == "telescopium") return QString("Tel"); if (fullName == "triangulum") return QString("Tri"); if (fullName == "triangulum australe") return QString("TrA"); if (fullName == "tucana") return QString("Tuc"); if (fullName == "ursa major") return QString("UMa"); if (fullName == "ursa minor") return QString("UMi"); if (fullName == "vela") return QString("Vel"); if (fullName == "virgo") return QString("Vir"); if (fullName == "volans") return QString("Vol"); if (fullName == "vulpecula") return QString("Vul"); return fullName_; } QString constGenetiveToAbbrev(const QString &genetive_) { QString genetive = genetive_.toLower(); if (genetive == "andromedae") return QString("And"); if (genetive == "antliae") return QString("Ant"); if (genetive == "apodis") return QString("Aps"); if (genetive == "aquarii") return QString("Aqr"); if (genetive == "aquilae") return QString("Aql"); if (genetive == "arae") return QString("Ara"); if (genetive == "arietis") return QString("Ari"); if (genetive == "aurigae") return QString("Aur"); if (genetive == "bootis") return QString("Boo"); if (genetive == "caeli") return QString("Cae"); if (genetive == "camelopardalis") return QString("Cam"); if (genetive == "cancri") return QString("Cnc"); if (genetive == "canum venaticorum") return QString("CVn"); if (genetive == "canis majoris") return QString("CMa"); if (genetive == "canis minoris") return QString("CMi"); if (genetive == "capricorni") return QString("Cap"); if (genetive == "carinae") return QString("Car"); if (genetive == "cassiopeiae") return QString("Cas"); if (genetive == "centauri") return QString("Cen"); if (genetive == "cephei") return QString("Cep"); if (genetive == "ceti") return QString("Cet"); if (genetive == "chamaeleontis") return QString("Cha"); if (genetive == "circini") return QString("Cir"); if (genetive == "columbae") return QString("Col"); if (genetive == "comae berenices") return QString("Com"); if (genetive == "coronae austrinae") return QString("CrA"); if (genetive == "coronae borealis") return QString("CrB"); if (genetive == "corvi") return QString("Crv"); if (genetive == "crateris") return QString("Crt"); if (genetive == "crucis") return QString("Cru"); if (genetive == "cygni") return QString("Cyg"); if (genetive == "delphini") return QString("Del"); if (genetive == "doradus") return QString("Dor"); if (genetive == "draconis") return QString("Dra"); if (genetive == "equulei") return QString("Equ"); if (genetive == "eridani") return QString("Eri"); if (genetive == "fornacis") return QString("For"); if (genetive == "geminorum") return QString("Gem"); if (genetive == "gruis") return QString("Gru"); if (genetive == "herculis") return QString("Her"); if (genetive == "horologii") return QString("Hor"); if (genetive == "hydrae") return QString("Hya"); if (genetive == "hydri") return QString("Hyi"); if (genetive == "indi") return QString("Ind"); if (genetive == "lacertae") return QString("Lac"); if (genetive == "leonis") return QString("Leo"); if (genetive == "leonis minoris") return QString("LMi"); if (genetive == "leporis") return QString("Lep"); if (genetive == "librae") return QString("Lib"); if (genetive == "lupi") return QString("Lup"); if (genetive == "lyncis") return QString("Lyn"); if (genetive == "lyrae") return QString("Lyr"); if (genetive == "mensae") return QString("Men"); if (genetive == "microscopii") return QString("Mic"); if (genetive == "monocerotis") return QString("Mon"); if (genetive == "muscae") return QString("Mus"); if (genetive == "normae") return QString("Nor"); if (genetive == "octantis") return QString("Oct"); if (genetive == "ophiuchi") return QString("Oph"); if (genetive == "orionis") return QString("Ori"); if (genetive == "pavonis") return QString("Pav"); if (genetive == "pegasi") return QString("Peg"); if (genetive == "persei") return QString("Per"); if (genetive == "phoenicis") return QString("Phe"); if (genetive == "pictoris") return QString("Pic"); if (genetive == "piscium") return QString("Psc"); if (genetive == "piscis austrini") return QString("PsA"); if (genetive == "puppis") return QString("Pup"); if (genetive == "pyxidis") return QString("Pyx"); if (genetive == "reticuli") return QString("Ret"); if (genetive == "sagittae") return QString("Sge"); if (genetive == "sagittarii") return QString("Sgr"); if (genetive == "scorpii") return QString("Sco"); if (genetive == "sculptoris") return QString("Scl"); if (genetive == "scuti") return QString("Sct"); if (genetive == "serpentis") return QString("Ser"); if (genetive == "sextantis") return QString("Sex"); if (genetive == "tauri") return QString("Tau"); if (genetive == "telescopii") return QString("Tel"); if (genetive == "trianguli") return QString("Tri"); if (genetive == "trianguli australis") return QString("TrA"); if (genetive == "tucanae") return QString("Tuc"); if (genetive == "ursae majoris") return QString("UMa"); if (genetive == "ursae minoris") return QString("UMi"); if (genetive == "velorum") return QString("Vel"); if (genetive == "virginis") return QString("Vir"); if (genetive == "volantis") return QString("Vol"); if (genetive == "vulpeculae") return QString("Vul"); return genetive_; } QString Logging::_filename; void Logging::UseFile() { if (_filename.isEmpty()) { QDir dir; QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "logs/" + QDateTime::currentDateTime().toString("yyyy-MM-dd"); dir.mkpath(path); QString name = "log_" + QDateTime::currentDateTime().toString("HH-mm-ss") + ".txt"; _filename = path + QStringLiteral("/") + name; // Clear file contents QFile file(_filename); file.open(QFile::WriteOnly); file.close(); } qSetMessagePattern("[%{time yyyy-MM-dd h:mm:ss.zzz t} %{if-debug}DEBG%{endif}%{if-info}INFO%{endif}%{if-warning}WARN%{endif}%{if-critical}CRIT%{endif}%{if-fatal}FATL%{endif}] %{if-category}[%{category}]%{endif} - %{message}"); qInstallMessageHandler(File); } void Logging::File(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QFile file(_filename); if (file.open(QFile::Append | QIODevice::Text)) { QTextStream stream(&file); Write(stream, type, context, msg); } } void Logging::UseStdout() { qSetMessagePattern("[%{time yyyy-MM-dd h:mm:ss.zzz t} %{if-debug}DEBG%{endif}%{if-info}INFO%{endif}%{if-warning}WARN%{endif}%{if-critical}CRIT%{endif}%{if-fatal}FATL%{endif}] %{if-category}[%{category}]%{endif} - %{message}"); qInstallMessageHandler(Stdout); } void Logging::Stdout(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QTextStream stream(stdout, QIODevice::WriteOnly); Write(stream, type, context, msg); } void Logging::UseStderr() { qInstallMessageHandler(Stderr); } void Logging::Stderr(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QTextStream stream(stderr, QIODevice::WriteOnly); Write(stream, type, context, msg); } void Logging::Write(QTextStream &stream, QtMsgType type, const QMessageLogContext &context, const QString &msg) { stream << QDateTime::currentDateTime().toString("[yyyy-MM-ddThh:mm:ss.zzz t "); switch (type) { case QtInfoMsg: stream << "INFO ]"; break; case QtDebugMsg: stream << "DEBG ]"; break; case QtWarningMsg: stream << "WARN ]"; break; case QtCriticalMsg: stream << "CRIT ]"; break; case QtFatalMsg: stream << "FATL ]"; break; default: stream << "UNKN ]"; } stream << "[" << qSetFieldWidth(30) << context.category << qSetFieldWidth(0) << "] - "; stream << msg << endl; //stream << qFormatLogMessage(type, context, msg) << endl; } void Logging::UseDefault() { qInstallMessageHandler(nullptr); } void Logging::Disable() { qInstallMessageHandler(Disabled); } void Logging::Disabled(QtMsgType, const QMessageLogContext &, const QString &) { } void Logging::SyncFilterRules() { // QString rules = QString("org.kde.kstars.ekos.debug=%1\n" // "org.kde.kstars.indi.debug=%2\n" // "org.kde.kstars.fits.debug=%3\n" // "org.kde.kstars.ekos.capture.debug=%4\n" // "org.kde.kstars.ekos.focus.debug=%5\n" // "org.kde.kstars.ekos.guide.debug=%6\n" // "org.kde.kstars.ekos.align.debug=%7\n" // "org.kde.kstars.ekos.mount.debug=%8\n" // "org.kde.kstars.ekos.scheduler.debug=%9\n").arg( // Options::verboseLogging() ? "true" : "false", // Options::iNDILogging() ? "true" : "false", // Options::fITSLogging() ? "true" : "false", // Options::captureLogging() ? "true" : "false", // Options::focusLogging() ? "true" : "false", // Options::guideLogging() ? "true" : "false", // Options::alignmentLogging() ? "true" : "false", // Options::mountLogging() ? "true" : "false", // Options::schedulerLogging() ? "true" : "false") // .append(QString("org.kde.kstars.ekos.observatory.debug=%2\n" // "org.kde.kstars.debug=%1").arg( // Options::verboseLogging() ? "true" : "false", // Options::observatoryLogging() ? "true" : "false")); QStringList rules; rules << "org.kde.kstars.ekos.debug" << (Options::verboseLogging() ? "true" : "false"); rules << "org.kde.kstars.indi.debug" << (Options::iNDILogging() ? "true" : "false"); rules << "org.kde.kstars.fits.debug" << (Options::fITSLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.capture.debug" << (Options::captureLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.focus.debug" << (Options::focusLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.guide.debug" << (Options::guideLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.align.debug" << (Options::alignmentLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.mount.debug" << (Options::mountLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.scheduler.debug" << (Options::schedulerLogging() ? "true" : "false"); rules << "org.kde.kstars.ekos.observatory.debug" << (Options::observatoryLogging() ? "true" : "false"); rules << "org.kde.kstars.debug" << (Options::verboseLogging() ? "true" : "false"); QString formattedRules; for (int i = 0; i < rules.size(); i += 2) formattedRules.append(QString("%1=%2\n").arg(rules[i], rules[i + 1])); QLoggingCategory::setFilterRules(formattedRules); } /** This method provides a centralized location for the default paths to important external files used in the Options on different operating systems. Note that on OS X, if the user builds the app without indi, astrometry, and xplanet internally then the options below will be used. If the user drags the app from a dmg and has to install the KStars data directory, then most of these paths will be overwritten since it is preferred to use the internal versions. **/ QString getDefaultPath(QString option) { QString snap = QProcessEnvironment::systemEnvironment().value("SNAP"); QString flat = QProcessEnvironment::systemEnvironment().value("FLATPAK_DEST"); if (option == "fitsDir") { return QDir::homePath(); } else if (option == "indiServer") { #ifdef Q_OS_OSX return "/usr/local/bin/indiserver"; #endif if (flat.isEmpty() == false) return flat + "/bin/indiserver"; else return snap + "/usr/bin/indiserver"; } else if (option == "indiDriversDir") { #ifdef Q_OS_OSX return "/usr/local/share/indi"; #elif defined(Q_OS_LINUX) if (flat.isEmpty() == false) return flat + "/share/indi"; else return snap + "/usr/share/indi"; #else return QStandardPaths::locate(QStandardPaths::GenericDataLocation, "indi", QStandardPaths::LocateDirectory); #endif } else if (option == "AstrometrySolverBinary") { #ifdef Q_OS_OSX return "/usr/local/bin/solve-field"; #endif if (flat.isEmpty() == false) return flat + "/bin/solve-field"; else return snap + "/usr/bin/solve-field"; } else if (option == "AstrometryWCSInfo") { #ifdef Q_OS_OSX return "/usr/local/bin/wcsinfo"; #endif if (flat.isEmpty() == false) return flat + "/bin/wcsinfo"; else return snap + "/usr/bin/wcsinfo"; } else if (option == "AstrometryConfFile") { #ifdef Q_OS_OSX return "/usr/local/etc/astrometry.cfg"; #endif if (flat.isEmpty() == false) return flat + "/etc/astrometry.cfg"; else return snap + "/etc/astrometry.cfg"; } else if (option == "AstrometryIndexFileLocation") { #ifdef Q_OS_OSX return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/Astrometry/"; #endif if (flat.isEmpty() == false) return flat + "/usr/share/astrometry/"; else return snap + "/usr/share/astrometry/"; } else if (option == "XplanetPath") { #ifdef Q_OS_OSX return "/usr/local/bin/xplanet"; #endif if (flat.isEmpty() == false) return flat + "/bin/xplanet"; else return snap + "/usr/bin/xplanet"; } return QString(); } #ifdef Q_OS_OSX //Note that this will copy and will not overwrite, so that the user's changes in the files are preserved. void copyResourcesFolderFromAppBundle(QString folder) { QString folderLocation = QStandardPaths::locate(QStandardPaths::GenericDataLocation, folder, QStandardPaths::LocateDirectory); QDir folderSourceDir; if(folder == "kstars") folderSourceDir = QDir(QCoreApplication::applicationDirPath() + "/../Resources/data").absolutePath(); else folderSourceDir = QDir(QCoreApplication::applicationDirPath() + "/../Resources/" + folder).absolutePath(); if (folderSourceDir.exists()) { folderLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + '/' + folder; QDir writableDir; writableDir.mkdir(folderLocation); copyRecursively(folderSourceDir.absolutePath(), folderLocation); } } bool copyDataFolderFromAppBundleIfNeeded() //The method returns true if the data directory is good to go. { //This will copy the locale folder, the notifications folder, and the sounds folder and any missing files in them to Application Support if needed. copyResourcesFolderFromAppBundle("locale"); copyResourcesFolderFromAppBundle("knotifications5"); copyResourcesFolderFromAppBundle("sounds"); //This will check for the data directory and if its not present, it will run the wizard. QString dataLocation = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kstars", QStandardPaths::LocateDirectory); if (dataLocation.isEmpty()) //If there is no kstars user data directory { QPointer wizard = new KSWizard(new QFrame()); wizard->exec(); //This will pause the startup until the user installs the data directory from the Wizard. dataLocation = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kstars", QStandardPaths::LocateDirectory); if (dataLocation.isEmpty()) return false; //This sets some important OS X options. Options::setIndiServerIsInternal(true); Options::setIndiServer("*Internal INDI Server*"); Options::setIndiDriversAreInternal(true); Options::setIndiDriversDir("*Internal INDI Drivers*"); Options::setAstrometrySolverIsInternal(true); Options::setAstrometrySolverBinary("*Internal Solver*"); Options::setAstrometryConfFileIsInternal(true); Options::setAstrometryConfFile("*Internal astrometry.cfg*"); Options::setAstrometryWCSIsInternal(true); Options::setAstrometryWCSInfo("*Internal wcsinfo*"); Options::setAstrometryUseNoFITS2FITS(false); Options::setXplanetIsInternal(true); Options::setXplanetPath("*Internal XPlanet*"); Options::setRunStartupWizard(false); //don't run on startup because we are doing it now. return true; //This means the data directory is good to go now that we created it from the default. } //This will copy any of the critical KStars files from the app bundle to application support if they are missing. copyResourcesFolderFromAppBundle("kstars"); return true; //This means the data directory was good to go from the start and the wizard did not run. } -bool getAstrometryDataDir(QString &dataDir) + +//Can this and the linux method be merged somehow? See KSUtils::createLocalAstrometryConf +bool configureAstrometry() +{ + QStringList astrometryDataDirs = getAstrometryDataDirs(); + if (astrometryDataDirs.count() == 0) + return false; + QString defaultAstrometryDataDir = getDefaultPath("AstrometryIndexFileLocation"); + if(astrometryDataDirs.contains("IndexFileLocationNotYetSet")) + replaceIndexFileNotYetSet(); + if (QDir(defaultAstrometryDataDir).exists() == false) + { + if (KMessageBox::warningYesNo( + nullptr, i18n("The selected Astrometry Index File Location:\n %1 \n does not exist. Do you want to make the directory?", defaultAstrometryDataDir), + i18n("Make Astrometry Index File Directory?")) == KMessageBox::Yes) + { + if(QDir(defaultAstrometryDataDir).mkdir(defaultAstrometryDataDir)) + { + KSNotification::info(i18n("The Default Astrometry Index File Location was created.")); + } + else + { + KSNotification::sorry(i18n("The Default Astrometry Index File Directory does not exist and was not able to be created.")); + } + } + else + { + return false; + } + } + + return true; +} + +bool replaceIndexFileNotYetSet() +{ + QString confPath = KSUtils::getAstrometryConfFilePath(); + + QFile confFile(confPath); + QString contents; + if (confFile.open(QIODevice::ReadOnly) == false) + { + KSNotification::error( i18n("Astrometry Configuration File Read Error.")); + return false; + } + else + { + QByteArray fileContent = confFile.readAll(); + confFile.close(); + QString contents = QString::fromLatin1(fileContent); + contents.replace("IndexFileLocationNotYetSet",getDefaultPath("AstrometryIndexFileLocation")); + + if (confFile.open(QIODevice::WriteOnly) == false) + { + KSNotification::error( i18n("Internal Astrometry Configuration File Write Error.")); + return false; + } + else + { + QTextStream out(&confFile); + out << contents; + confFile.close(); + } + } + return true; +} + +bool copyRecursively(QString sourceFolder, QString destFolder) +{ + QDir sourceDir(sourceFolder); + + if (!sourceDir.exists()) + return false; + + QDir destDir(destFolder); + if (!destDir.exists()) + destDir.mkdir(destFolder); + + QStringList files = sourceDir.entryList(QDir::Files); + for (int i = 0; i < files.count(); i++) + { + QString srcName = sourceFolder + QDir::separator() + files[i]; + QString destName = destFolder + QDir::separator() + files[i]; + QFile::copy(srcName, destName); //Note this does not overwrite files + } + + files.clear(); + files = sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + for (int i = 0; i < files.count(); i++) + { + QString srcName = sourceFolder + QDir::separator() + files[i]; + QString destName = destFolder + QDir::separator() + files[i]; + copyRecursively(srcName, destName); + } + + return true; +} +#endif + +//Note maybe the Mac and Linux versions of creating the local astrometry conf file and index file folder can be merged. +//I moved both of them here and this method references each one separately. +//One is createLocalAstrometryConf and the other is configureAstrometry +bool configureLocalAstrometryConfIfNecessary() +{ +#ifdef Q_OS_LINUX + QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); + if (QFileInfo(confPath).exists() == false) + { + if(createLocalAstrometryConf() == false) + return false; + } + +#else //Mac + if(configureAstrometry() == false) + { + KMessageBox::information( + nullptr, + i18n( + "Failed to properly configure astrometry config file. Please click the options button in the lower right of the Astrometry Tab in Ekos to correct your settings. Then try starting Ekos again."), + i18n("Astrometry Config File Error"), "astrometry_configuration_failure_warning"); + return false; + } +#endif + return true; +} + +//Can this and the mac method be merged somehow? See KSUtils::configureAstrometry. +bool createLocalAstrometryConf() { - QString confPath; + bool rc = false; + QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); + QString systemConfPath = "/etc/astrometry.cfg"; + + // Check if directory already exists, if it doesn't create one + QDir writableDir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); + if (writableDir.exists() == false) + { + rc = writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); + + if (rc == false) + { + qCCritical(KSTARS_EKOS_ALIGN) << "Failed to create local astrometry directory"; + return false; + } + } + + // Now copy system astrometry.cfg to local directory + rc = QFile(systemConfPath).copy(confPath); + + if (rc == false) + { + qCCritical(KSTARS_EKOS_ALIGN) << "Failed to copy" << systemConfPath << "to" << confPath; + return false; + } + + QFile localConf(confPath); + + // Open file and add our own path to it + if (localConf.open(QFile::ReadWrite)) + { + QString all = localConf.readAll(); + QStringList lines = all.split("\n"); + for (int i = 0; i < lines.count(); i++) + { + if (lines[i].startsWith("add_path")) + { + lines.insert(i + 1, QString("add_path %1astrometry").arg(KSPaths::writableLocation(QStandardPaths::GenericDataLocation))); + break; + } + } + + // Clear contents + localConf.resize(0); + + // Now write back all the lines including our own inserted above + QTextStream out(&localConf); + for(const QString &line : lines) + out << line << endl; + localConf.close(); + return true; + } + + qCCritical(KSTARS_EKOS_ALIGN) << "Failed to open local astrometry config" << confPath; + return false; +} + +QString getAstrometryConfFilePath() +{ if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; + #ifdef Q_OS_LINUX + return KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); + #else //Mac + return QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; + #endif else - confPath = Options::astrometryConfFile(); + return Options::astrometryConfFile(); +} + +QStringList getAstrometryDataDirs() +{ + QStringList dataDirs; + QString confPath = KSUtils::getAstrometryConfFilePath(); QFile confFile(confPath); if (confFile.open(QIODevice::ReadOnly) == false) { - KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " - "configuration file full path in INDI options.", - Options::astrometryConfFile())); - return false; + bool confFileExists = false; + if(Options::astrometryConfFileIsInternal()) + { + if(KSUtils::configureLocalAstrometryConfIfNecessary()) + { + if (confFile.open(QIODevice::ReadOnly)) + confFileExists = true; + } + } + if(!confFileExists) + { + KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " + "configuration file full path in INDI options.", + confPath)); + return dataDirs; + } } QTextStream in(&confFile); QString line; while (!in.atEnd()) { line = in.readLine(); if (line.isEmpty() || line.startsWith('#')) continue; line = line.trimmed(); if (line.startsWith(QLatin1String("add_path"))) { - dataDir = line.trimmed().mid(9).trimmed(); - return true; + dataDirs << line.mid(9).trimmed(); } } + // if(dataDirs.size()==0) + // KSNotification::error(i18n("Unable to find data dir in astrometry configuration file.")); - KSNotification::error(i18n("Unable to find data dir in astrometry configuration file.")); - return false; + return dataDirs; } -bool setAstrometryDataDir(QString dataDir) +bool addAstrometryDataDir(QString dataDir) { - if(Options::astrometryIndexFileLocation() != dataDir) - Options::setAstrometryIndexFileLocation(dataDir); - QString confPath; - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - confPath = Options::astrometryConfFile(); + //This will need to be fixed! + //if(Options::astrometryIndexFileLocation() != dataDir) + // Options::setAstrometryIndexFileLocation(dataDir); + + QString confPath = KSUtils::getAstrometryConfFilePath(); + QStringList astrometryDataDirs = getAstrometryDataDirs(); QFile confFile(confPath); QString contents; if (confFile.open(QIODevice::ReadOnly) == false) { KSNotification::error( i18n("Astrometry Configuration File Read Error.")); return false; } else { QTextStream in(&confFile); QString line; - bool foundPathBefore = false; - bool fileNeedsUpdating = false; + bool foundSpot = false; while (!in.atEnd()) { line = in.readLine(); + if (line.trimmed().startsWith(QLatin1String("add_path"))) { - if (!foundPathBefore) //This will ensure there is not more than one add_path line in the file. + if (!foundSpot) { - foundPathBefore = true; - QString dataDirInFile = line.trimmed().mid(9).trimmed(); - if (dataDirInFile != dataDir) //Update to the correct path. - { - contents += "add_path " + dataDir + '\n'; - fileNeedsUpdating = true; - } + foundSpot = true; + for(QString astrometryDataDir:astrometryDataDirs) + contents += "add_path " + astrometryDataDir + '\n'; + contents += "add_path " + dataDir + '\n'; + } + else + { + //Do not keep adding the other add_paths because they just got added in the seciton above. } } else { contents += line + '\n'; } } + if(!foundSpot) + { + for(QString astrometryDataDir:astrometryDataDirs) + contents += "add_path " + astrometryDataDir + '\n'; + contents += "add_path " + dataDir + '\n'; + } + confFile.close(); - if (fileNeedsUpdating) + + if (confFile.open(QIODevice::WriteOnly) == false) { - if (confFile.open(QIODevice::WriteOnly) == false) - { - KSNotification::error( i18n("Internal Astrometry Configuration File Write Error.")); - return false; - } - else - { - QTextStream out(&confFile); - out << contents; - confFile.close(); - } + KSNotification::error( i18n("Internal Astrometry Configuration File Write Error.")); + return false; + } + else + { + QTextStream out(&confFile); + out << contents; + confFile.close(); } } return true; } -bool configureAstrometry() +bool removeAstrometryDataDir(QString dataDir) { - QString astrometryDataDir; - if (KSUtils::getAstrometryDataDir(astrometryDataDir) == false) - return false; - if(astrometryDataDir == "IndexFileLocationNotYetSet") + QString confPath = KSUtils::getAstrometryConfFilePath(); + QStringList astrometryDataDirs = getAstrometryDataDirs(); + + QFile confFile(confPath); + QString contents; + if (confFile.open(QIODevice::ReadOnly) == false) { - astrometryDataDir = Options::astrometryIndexFileLocation(); - setAstrometryDataDir(astrometryDataDir); + KSNotification::error( i18n("Astrometry Configuration File Read Error.")); + return false; } - if(Options::astrometryIndexFileLocation() != astrometryDataDir) + else { - if (KMessageBox::warningYesNo( - nullptr, i18n("The Astrometry Index File Location Stored in KStars: \n %1 \n does not match the Index file location in the config file: \n %2 \n Do you want to update the config file?", Options::astrometryIndexFileLocation(), astrometryDataDir), - i18n("Update Config File?")) == KMessageBox::Yes) + QTextStream in(&confFile); + QString line; + while (!in.atEnd()) { - astrometryDataDir = Options::astrometryIndexFileLocation(); - setAstrometryDataDir(astrometryDataDir); + line = in.readLine(); + if (line.mid(9).trimmed() != dataDir) + { + contents += line + '\n'; + } + } - else + confFile.close(); + + if (confFile.open(QIODevice::WriteOnly) == false) { + KSNotification::error( i18n("Internal Astrometry Configuration File Write Error.")); return false; } - } - if (QDir(astrometryDataDir).exists() == false) - { - if (KMessageBox::warningYesNo( - nullptr, i18n("The selected Astrometry Index File Location:\n %1 \n does not exist. Do you want to make the directory?", astrometryDataDir), - i18n("Make Astrometry Index File Directory?")) == KMessageBox::Yes) - { - if(QDir(astrometryDataDir).mkdir(astrometryDataDir)) - { - KSNotification::info(i18n("The Astrometry Index File Location was created.")); - } - else - { - KSNotification::sorry(i18n("The Astrometry Index File Directory does not exist and was not able to be created.")); - } - } else { - - return false; + QTextStream out(&confFile); + out << contents; + confFile.close(); } } - - //If the Index File Directories match and the directory exists, we are good to go. return true; } - -bool copyRecursively(QString sourceFolder, QString destFolder) -{ - QDir sourceDir(sourceFolder); - - if (!sourceDir.exists()) - return false; - - QDir destDir(destFolder); - if (!destDir.exists()) - destDir.mkdir(destFolder); - - QStringList files = sourceDir.entryList(QDir::Files); - for (int i = 0; i < files.count(); i++) - { - QString srcName = sourceFolder + QDir::separator() + files[i]; - QString destName = destFolder + QDir::separator() + files[i]; - QFile::copy(srcName, destName); //Note this does not overwrite files - } - - files.clear(); - files = sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); - for (int i = 0; i < files.count(); i++) - { - QString srcName = sourceFolder + QDir::separator() + files[i]; - QString destName = destFolder + QDir::separator() + files[i]; - copyRecursively(srcName, destName); - } - - return true; -} -#endif - QByteArray getJPLQueryString(const QByteArray &kind, const QByteArray &dataFields, const QVector &filters) { QByteArray query("obj_group=all&obj_kind=" + kind + "&obj_numbered=all&OBJ_field=0&ORB_field=0"); // Apply filters: for (int i = 0; i < filters.length(); i++) { QByteArray f = QByteArray::number(i + 1); query += "&c" + f + "_group=OBJ&c1_item=" + filters[i].item + "&c" + f + "_op=" + filters[i].op + "&c" + f + "_value=" + filters[i].value; } // Apply query data fields... query += "&c_fields=" + dataFields; query += "&table_format=CSV&max_rows=10&format_option=full&query=Generate%20Table&." "cgifields=format_option&.cgifields=field_list&.cgifields=obj_kind&.cgifie" "lds=obj_group&.cgifields=obj_numbered&.cgifields=combine_mode&.cgifields=" "ast_orbit_class&.cgifields=table_format&.cgifields=ORB_field_set&.cgifiel" "ds=OBJ_field_set&.cgifields=preset_field_set&.cgifields=com_orbit_class"; return query; } bool RAWToJPEG(const QString &rawImage, const QString &output, QString &errorMessage) { #ifndef HAVE_LIBRAW errorMessage = i18n("Unable to find dcraw and cjpeg. Please install the required tools to convert CR2/NEF to JPEG."); return false; #else int ret = 0; // Creation of image processing object LibRaw RawProcessor; // Let us open the file if ((ret = RawProcessor.open_file(rawImage.toLatin1().data())) != LIBRAW_SUCCESS) { errorMessage = i18n("Cannot open %1: %2", rawImage, libraw_strerror(ret)); RawProcessor.recycle(); return false; } // Let us unpack the thumbnail if ((ret = RawProcessor.unpack_thumb()) != LIBRAW_SUCCESS) { errorMessage = i18n("Cannot unpack_thumb %1: %2", rawImage, libraw_strerror(ret)); RawProcessor.recycle(); return false; } else // We have successfully unpacked the thumbnail, now let us write it to a file { //snprintf(thumbfn,sizeof(thumbfn),"%s.%s",av[i],T.tformat == LIBRAW_THUMBNAIL_JPEG ? "thumb.jpg" : "thumb.ppm"); if (LIBRAW_SUCCESS != (ret = RawProcessor.dcraw_thumb_writer(output.toLatin1().data()))) { errorMessage = i18n("Cannot write %s %1: %2", output, libraw_strerror(ret)); RawProcessor.recycle(); return false; } } return true; #endif } } diff --git a/kstars/auxiliary/ksutils.h b/kstars/auxiliary/ksutils.h index 6c8329e18..962013043 100644 --- a/kstars/auxiliary/ksutils.h +++ b/kstars/auxiliary/ksutils.h @@ -1,290 +1,297 @@ /*************************************************************************** kstars.h - K Desktop Planetarium ------------------- begin : Mon Jan 7 2002 copyright : (C) 2002 by Mark Hollomon email : mhh@mindspring.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ /** @class KSUtils *@short KStars utility functions *@author Mark Hollomon *@version 1.0 *Static functions for various purposes. *The openDataFile() function searches the standard KDE directories *for the data filename given as an argument. *(it is impossible to instantiate a KSUtils object; just use the static functions). */ #pragma once #include "dms.h" #include #include #if __GNUC__ > 5 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wignored-attributes" #endif #if __GNUC__ > 6 #pragma GCC diagnostic ignored "-Wint-in-bool-context" #endif #include #if __GNUC__ > 5 #pragma GCC diagnostic pop #endif #include class QFile; class QString; class QTextStream; class SkyPoint; class SkyObject; class StarObject; namespace KSUtils { // Quick checks whether hardware is limited or not // right now the only check is architecture. arm processors are limited while x86 are sufficient bool isHardwareLimited(); /** Attempt to open the data file named filename, using the QFile object "file". *First look in the standard KDE directories, then look in a local "data" *subdirectory. If the data file cannot be found or opened, display a warning *messagebox. *@short Open a data file. *@param file The QFile object to be opened *@param s The name of the data file. *@returns bool Returns true if data file was opened successfully. *@returns a reference to the opened file. */ bool openDataFile(QFile &file, const QString &s); /** Clamp value into range. * @p x value to clamp. * @p min minimal allowed value. * @p max maximum allowed value. */ template inline T clamp(T x, T min, T max) { if (x < min) return min; if (x > max) return max; return x; } /** Put angle into range. Period is equal to max-min. * * @p x angle value * @p min minimal angle * @p max maximal angle */ template inline T reduceAngle(T x, T min, T max) { T delta = max - min; return x - delta * floor((x - min) / delta); } /** Convert from spherical to cartesian coordinate system. * Resulting vector have unit length */ inline Eigen::Vector3d fromSperical(const dms &longitude, const dms &latitude) { double sinL, sinB; double cosL, cosB; longitude.SinCos(sinL, cosL); latitude.SinCos(sinB, cosB); return Eigen::Vector3d(cosB * cosL, cosB * sinL, sinB); } /** Convert a vector to a point */ inline QPointF vecToPoint(const Eigen::Vector2f &vec) { return QPointF(vec[0], vec[1]); } /** Convert a point to a vector */ inline Eigen::Vector2f pointToVec(const QPointF &p) { return Eigen::Vector2f(p.x(), p.y()); } /** *@short Create a URL to obtain a DSS image for a given SkyPoint *@note If SkyPoint is a DeepSkyObject, this method automatically *decides the image size required to fit the object. */ QString getDSSURL(const SkyPoint *const p); /** *@short Create a URL to obtain a DSS image for a given RA, Dec *@param ra The J2000.0 Right Ascension of the point *@param dec The J2000.0 Declination of the point *@param width The width of the image in arcminutes *@param height The height of the image in arcminutes *@param type The image type, either gif or fits. *@note This method resets height and width to fall within the range accepted by DSS */ QString getDSSURL(const dms &ra, const dms &dec, float width = 0, float height = 0, const QString &type = "gif"); /** *@short Return a string corresponding to an angle specifying direction * * The angle must measure direction from North, towards East. Both * the azimuth and position angle follow this convention, so this * method can be used to return a string corresponding to the * general heading of a given azimuth / position angle. * *@param angle angle as dms (measured from North, towards East) *@return A localized string corresponding to the approximate direction (eg: NNW) */ QString toDirectionString(dms angle); /** *@short Converts StarObject list into SkyObject list *@param starObjList QList of StarObject pointers *@return Returns a pointer to QList of SkyObject pointers *@note Used for Star-Hopper */ QList *castStarObjListToSkyObjList(QList *starObjList); /** *@note Avoid using this method for the same reasons as QSharedPointer::data() */ template QList makeVanillaPointerList(const QList> &spList) { QList vpList; foreach (QSharedPointer sp, spList) vpList.append(sp.data()); return vpList; } /** *@short Return the genetive form of constellation name, given the abbreviation *@param code Three-letter IAU abbreviation of the constellation *@return the genetive form of the constellation name */ QString constGenetiveFromAbbrev(const QString &code); /** *@short Return the name of the constellation, given the abbreviation *@param code Three-letter IAU abbreviation of the constellation *@return the nominative form of the constellation name */ QString constNameFromAbbrev(const QString &code); /** *@short Return the abbreviation of the constellation, given the full name *@param fullName_ Full name of the constellation *@return the three-letter IAU standard abbreviation of the constellation */ QString constNameToAbbrev(const QString &fullName_); /** *@short Return the abbreviation of the constellation, given the genetive form *@param genetive_ the genetive form of the constellation's name *@return the three-letter IAU standard abbreviation of the constellation */ QString constGenetiveToAbbrev(const QString &genetive_); /** * Interface into Qt's logging system * @author: Yale Dedis 2011 * Adapted from DeDiS project. */ class Logging { public: /** * Store all logs into the specified file */ static void UseFile(); /** * Output logs to stdout */ static void UseStdout(); /** * Output logs to stderr */ static void UseStderr(); /** * Use the default logging mechanism */ static void UseDefault(); /** * Disable logging */ static void Disable(); /** * @brief SyncFilterRules Sync QtLogging filter rules from Options */ static void SyncFilterRules(); private: static QString _filename; static void Disabled(QtMsgType type, const QMessageLogContext &context, const QString &msg); static void File(QtMsgType type, const QMessageLogContext &context, const QString &msg); static void Stdout(QtMsgType type, const QMessageLogContext &context, const QString &msg); static void Stderr(QtMsgType type, const QMessageLogContext &context, const QString &msg); static void Write(QTextStream &stream, QtMsgType type, const QMessageLogContext &context, const QString &msg); }; QString getDefaultPath(QString option); #ifdef Q_OS_OSX void copyResourcesFolderFromAppBundle(QString folder); bool copyDataFolderFromAppBundleIfNeeded(); //The boolean returns true if the data folders are good to go. -bool getAstrometryDataDir(QString &dataDir); -bool setAstrometryDataDir(QString dataDir); bool configureAstrometry(); +bool replaceIndexFileNotYetSet(); bool copyRecursively(QString sourceFolder, QString destFolder); #endif +bool configureLocalAstrometryConfIfNecessary(); +bool createLocalAstrometryConf(); +QString getAstrometryConfFilePath(); +QStringList getAstrometryDataDirs(); +bool addAstrometryDataDir(QString dataDir); +bool removeAstrometryDataDir(QString dataDir); + + struct JPLFilter { QByteArray item; QByteArray op; QByteArray value; }; // TODO: Implement Datatypes//Maps for kind, datafields, filters... /** *@short Generate a query string for downloading comet/asteroid data from JPL. *@param kind The kind of object we want: ast, com. *@param dataFields The columns we want to download. *@param filters Filters for the Data. *@return The query string. */ QByteArray getJPLQueryString(const QByteArray &kind, const QByteArray &dataFields, const QVector &filters); /** * @brief RAWToJPEG Convert raw image (e.g. CR2) using libraw to a JPEG image * @param rawImage full path to raw image * @param output full path to jpeg image to write to * @return True if conversion is successful, false otherwise. */ bool RAWToJPEG(const QString &rawImage, const QString &output, QString &errorMessage); } diff --git a/kstars/ekos/align/align.cpp b/kstars/ekos/align/align.cpp index cf439c655..606c92075 100644 --- a/kstars/ekos/align/align.cpp +++ b/kstars/ekos/align/align.cpp @@ -1,6113 +1,6111 @@ /* Ekos Alignment Module Copyright (C) 2013 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "align.h" #include "alignadaptor.h" #include "alignview.h" #include "flagcomponent.h" #include "fov.h" #include "kstars.h" #include "kstarsdata.h" #include "ksuserdb.h" #include "offlineastrometryparser.h" #include "onlineastrometryparser.h" #include "opsalign.h" #include "opsastrometry.h" #include "opsastrometrycfg.h" #include "opsastrometryindexfiles.h" #include "Options.h" #include "remoteastrometryparser.h" #include "skymap.h" #include "skymapcomposite.h" #include "starobject.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/indifilter.h" #include "profileinfo.h" #include "ksnotification.h" #include #include #include #include #include #define PAH_CUTOFF_FOV 10 // Minimum FOV width in arcminutes for PAH to work #define MAXIMUM_SOLVER_ITERATIONS 10 #define AL_FORMAT_VERSION 1.0 namespace Ekos { // 30 arcmiutes RA movement const double Align::RAMotion = 0.5; // Sidereal rate, degrees/s const double Align::SIDRATE = 0.004178; const QMap Align::PAHStages = { {PAH_IDLE, I18N_NOOP("Idle")}, {PAH_FIRST_CAPTURE, I18N_NOOP("First Capture"}), {PAH_FIND_CP, I18N_NOOP("Finding CP"}), {PAH_FIRST_ROTATE, I18N_NOOP("First Rotation"}), {PAH_SECOND_CAPTURE, I18N_NOOP("Second Capture"}), {PAH_SECOND_ROTATE, I18N_NOOP("Second Rotation"}), {PAH_THIRD_CAPTURE, I18N_NOOP("Third Capture"}), {PAH_STAR_SELECT, I18N_NOOP("Select Star"}), {PAH_PRE_REFRESH, I18N_NOOP("Select Refresh"}), {PAH_REFRESH, I18N_NOOP("Refreshing"}), {PAH_ERROR, I18N_NOOP("Error")}, }; Align::Align(ProfileInfo *activeProfile) : m_ActiveProfile(activeProfile) { setupUi(this); qRegisterMetaType("Ekos::AlignState"); qDBusRegisterMetaType(); new AlignAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Align", this); dirPath = QDir::homePath(); //loadSlewMode = false; solverFOV.reset(new FOV()); solverFOV->setName(i18n("Solver FOV")); solverFOV->setLockCelestialPole(true); solverFOV->setColor(KStars::Instance()->data()->colorScheme()->colorNamed("SolverFOVColor").name()); sensorFOV.reset(new FOV()); sensorFOV->setLockCelestialPole(true); QAction *a = KStars::Instance()->actionCollection()->action("show_sensor_fov"); if (a) a->setEnabled(true); showFITSViewerB->setIcon( QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Align::showFITSViewer); toggleFullScreenB->setIcon( QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Align::toggleAlignWidgetFullScreen); alignView = new AlignView(alignWidget, FITS_ALIGN); alignView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); alignView->setBaseSize(alignWidget->size()); alignView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(alignView); alignWidget->setLayout(vlayout); connect(solveB, &QPushButton::clicked, this, &Ekos::Align::captureAndSolve); connect(stopB, &QPushButton::clicked, this, &Ekos::Align::abort); connect(measureAltB, &QPushButton::clicked, this, &Ekos::Align::measureAltError); connect(measureAzB, &QPushButton::clicked, this, &Ekos::Align::measureAzError); // Effective FOV Edit connect(FOVOut, &QLineEdit::editingFinished, this, &Align::syncFOV); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::setDefaultCCD); connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkCCD); connect(correctAltB, &QPushButton::clicked, this, &Ekos::Align::correctAltError); connect(correctAzB, &QPushButton::clicked, this, &Ekos::Align::correctAzError); connect(loadSlewB, &QPushButton::clicked, [&]() { loadAndSlew(); }); FilterDevicesCombo->addItem("--"); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), [ = ](const QString & text) { syncSettings(); Options::setDefaultAlignFW(text); }); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Align::checkFilter); connect(FilterPosCombo, static_cast(&QComboBox::activated), [ = ](int index) { syncSettings(); Options::setLockAlignFilterIndex(index); } ); connect(PAHSlewRateCombo, static_cast(&QComboBox::activated), [&](int index) { Options::setPAHMountSpeedIndex(index); }); gotoModeButtonGroup->setId(syncR, GOTO_SYNC); gotoModeButtonGroup->setId(slewR, GOTO_SLEW); gotoModeButtonGroup->setId(nothingR, GOTO_NOTHING); connect(gotoModeButtonGroup, static_cast(&QButtonGroup::buttonClicked), this, [ = ](int id) { this->currentGotoMode = static_cast(id); }); m_CaptureTimer.setSingleShot(true); m_CaptureTimer.setInterval(10000); connect(&m_CaptureTimer, &QTimer::timeout, [&]() { if (m_CaptureTimeoutCounter++ > 3) { appendLogText(i18n("Capture timed out.")); abort(); } else { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip->isCapturing()) { targetChip->abortExposure(); m_CaptureTimer.start( m_CaptureTimer.interval() * 2); } else captureAndSolve(); } }); m_AlignTimer.setSingleShot(true); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); connect(&m_AlignTimer, &QTimer::timeout, this, &Ekos::Align::checkAlignmentTimeout); currentGotoMode = static_cast(Options::solverGotoOption()); gotoModeButtonGroup->button(currentGotoMode)->setChecked(true); editOptionsB->setIcon(QIcon::fromTheme("document-edit")); editOptionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); KConfigDialog *dialog = new KConfigDialog(this, "alignsettings", Options::self()); #ifdef Q_OS_OSX dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif opsAlign = new OpsAlign(this); connect(opsAlign, &OpsAlign::settingsUpdated, this, &Ekos::Align::refreshAlignOptions); KPageWidgetItem *page = dialog->addPage(opsAlign, i18n("Astrometry.net")); page->setIcon(QIcon(":/icons/astrometry.svg")); opsAstrometry = new OpsAstrometry(this); page = dialog->addPage(opsAstrometry, i18n("Solver Options")); page->setIcon(QIcon::fromTheme("configure")); -#ifdef Q_OS_OSX opsAstrometryCfg = new OpsAstrometryCfg(this); page = dialog->addPage(opsAstrometryCfg, i18n("Astrometry.cfg")); page->setIcon(QIcon::fromTheme("document-edit")); -#endif #ifndef Q_OS_WIN opsAstrometryIndexFiles = new OpsAstrometryIndexFiles(this); page = dialog->addPage(opsAstrometryIndexFiles, i18n("Index Files")); page->setIcon(QIcon::fromTheme("map-flat")); #endif connect(editOptionsB, &QPushButton::clicked, dialog, &QDialog::show); appendLogText(i18n("Idle.")); pi.reset(new QProgressIndicator(this)); stopLayout->addWidget(pi.get()); exposureIN->setValue(Options::alignExposure()); connect(exposureIN, static_cast(&QDoubleSpinBox::valueChanged), [&]() { syncSettings(); }); altStage = ALT_INIT; azStage = AZ_INIT; rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); // Online/Offline/Remote solver check solverTypeGroup->setId(onlineSolverR, SOLVER_ONLINE); solverTypeGroup->setId(offlineSolverR, SOLVER_OFFLINE); solverTypeGroup->setId(remoteSolverR, SOLVER_REMOTE); #ifdef Q_OS_WIN offlineSolverR->setEnabled(false); offlineSolverR->setToolTip( i18n("Offline solver is not supported under Windows. Please use either the Online or Remote solvers.")); #endif solverTypeGroup->button(Options::solverType())->setChecked(true); connect(solverTypeGroup, static_cast(&QButtonGroup::buttonClicked), this, &Align::setSolverType); switch (solverTypeGroup->checkedId()) { case SOLVER_ONLINE: onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: offlineParser.reset(new OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: remoteParser.reset(new RemoteAstrometryParser()); parser = remoteParser.get(); break; } parser->setAlign(this); if (parser->init() == false) setEnabled(false); else { connect(parser, &Ekos::AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &Ekos::AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } //solverOptions->setText(Options::solverOptions()); // Which telescope info to use for FOV calculations //kcfg_solverOTA->setChecked(Options::solverOTA()); //guideScopeCCDs = Options::guideScopeCCDs(); FOVScopeCombo->setCurrentIndex(Options::solverScopeType()); connect(FOVScopeCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::updateTelescopeType); //connect(FOVScopeCombo, SIGNAL(currentIndexChanged(int)), this, SIGNAL(newFOVTelescopeType(int))); accuracySpin->setValue(Options::solverAccuracyThreshold()); alignDarkFrameCheck->setChecked(Options::alignDarkFrame()); delaySpin->setValue(Options::settlingTime()); connect(delaySpin, &QSpinBox::editingFinished, this, &Ekos::Align::saveSettleTime); connect(binningCombo, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::setBinningIndex); // PAH Connections connect(this, &Align::PAHEnabled, [&](bool enabled) { PAHStartB->setEnabled(enabled); directionLabel->setEnabled(enabled); PAHDirectionCombo->setEnabled(enabled); PAHRotationSpin->setEnabled(enabled); PAHSlewRateCombo->setEnabled(enabled); PAHManual->setEnabled(enabled); }); connect(PAHStartB, &QPushButton::clicked, this, &Ekos::Align::startPAHProcess); // PAH StopB is just a shortcut for the regular stop connect(PAHStopB, &QPushButton::clicked, this, &Align::stopPAHProcess); connect(PAHCorrectionsNextB, &QPushButton::clicked, this, &Ekos::Align::setPAHCorrectionSelectionComplete); connect(PAHRefreshB, &QPushButton::clicked, this, &Ekos::Align::startPAHRefreshProcess); connect(PAHDoneB, &QPushButton::clicked, this, &Ekos::Align::setPAHRefreshComplete); // done buttons for manual slewing during polar alignment: connect(PAHfirstDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); connect(PAHsecondDone, &QPushButton::clicked, this, &Ekos::Align::setPAHSlewDone); if (solverOptions->text().contains("no-fits2fits")) appendLogText(i18n( "Warning: If using astrometry.net v0.68 or above, remove the --no-fits2fits from the astrometry options.")); hemisphere = KStarsData::Instance()->geo()->lat()->Degrees() > 0 ? NORTH_HEMISPHERE : SOUTH_HEMISPHERE; double accuracyRadius = accuracySpin->value(); alignPlot->setBackground(QBrush(Qt::black)); alignPlot->setSelectionTolerance(10); alignPlot->xAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->yAxis->setBasePen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); alignPlot->xAxis->setTickLabelColor(Qt::white); alignPlot->yAxis->setTickLabelColor(Qt::white); alignPlot->xAxis->setLabelColor(Qt::white); alignPlot->yAxis->setLabelColor(Qt::white); alignPlot->xAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->yAxis->setLabelFont(QFont(font().family(), 10)); alignPlot->xAxis->setLabelPadding(2); alignPlot->yAxis->setLabelPadding(2); alignPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); alignPlot->xAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->yAxis->grid()->setZeroLinePen(QPen(Qt::yellow)); alignPlot->xAxis->setLabel(i18n("dRA (arcsec)")); alignPlot->yAxis->setLabel(i18n("dDE (arcsec)")); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->setInteractions(QCP::iRangeZoom); alignPlot->setInteraction(QCP::iRangeDrag, true); alignPlot->addGraph(); alignPlot->graph(0)->setLineStyle(QCPGraph::lsNone); alignPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, Qt::white, 15)); buildTarget(); connect(alignPlot, &QCustomPlot::mouseMove, this, &Ekos::Align::handlePointTooltip); connect(rightLayout, &QSplitter::splitterMoved, this, &Ekos::Align::handleVerticalPlotSizeChange); connect(alignSplitter, &QSplitter::splitterMoved, this, &Ekos::Align::handleHorizontalPlotSizeChange); connect(accuracySpin, static_cast(&QSpinBox::valueChanged), this, &Ekos::Align::buildTarget); alignPlot->resize(190, 190); alignPlot->replot(); solutionTable->setColumnWidth(0, 70); solutionTable->setColumnWidth(1, 75); solutionTable->setColumnWidth(2, 80); solutionTable->setColumnWidth(3, 30); solutionTable->setColumnWidth(4, 100); solutionTable->setColumnWidth(5, 100); clearAllSolutionsB->setIcon( QIcon::fromTheme("application-exit")); clearAllSolutionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeSolutionB->setIcon(QIcon::fromTheme("list-remove")); removeSolutionB->setAttribute(Qt::WA_LayoutUsesWidgetRect); exportSolutionsCSV->setIcon( QIcon::fromTheme("document-save-as")); exportSolutionsCSV->setAttribute(Qt::WA_LayoutUsesWidgetRect); autoScaleGraphB->setIcon(QIcon::fromTheme("zoom-fit-best")); autoScaleGraphB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.setupUi(&mountModelDialog); mountModelDialog.setWindowTitle("Mount Model Tool"); mountModelDialog.setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); mountModel.alignTable->setColumnWidth(0, 70); mountModel.alignTable->setColumnWidth(1, 75); mountModel.alignTable->setColumnWidth(2, 130); mountModel.alignTable->setColumnWidth(3, 30); mountModel.wizardAlignB->setIcon( QIcon::fromTheme("tools-wizard")); mountModel.wizardAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.clearAllAlignB->setIcon( QIcon::fromTheme("application-exit")); mountModel.clearAllAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.removeAlignB->setIcon(QIcon::fromTheme("list-remove")); mountModel.removeAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.addAlignB->setIcon(QIcon::fromTheme("list-add")); mountModel.addAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.findAlignB->setIcon(QIcon::fromTheme("edit-find")); mountModel.findAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.alignTable->verticalHeader()->setDragDropOverwriteMode(false); mountModel.alignTable->verticalHeader()->setSectionsMovable(true); mountModel.alignTable->verticalHeader()->setDragEnabled(true); mountModel.alignTable->verticalHeader()->setDragDropMode(QAbstractItemView::InternalMove); connect(mountModel.alignTable->verticalHeader(), SIGNAL(sectionMoved(int, int, int)), this, SLOT(moveAlignPoint(int, int, int))); mountModel.loadAlignB->setIcon( QIcon::fromTheme("document-open")); mountModel.loadAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAsAlignB->setIcon( QIcon::fromTheme("document-save-as")); mountModel.saveAsAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.saveAlignB->setIcon( QIcon::fromTheme("document-save")); mountModel.saveAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setIcon(QIcon::fromTheme("kstars_grid")); mountModel.previewB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.previewB->setCheckable(true); mountModel.sortAlignB->setIcon(QIcon::fromTheme("svn-update")); mountModel.sortAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.stopAlignB->setIcon( QIcon::fromTheme("media-playback-stop")); mountModel.stopAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.startAlignB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(clearAllSolutionsB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllSolutionPoints); connect(removeSolutionB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveSolutionPoint); connect(exportSolutionsCSV, &QPushButton::clicked, this, &Ekos::Align::exportSolutionPoints); connect(autoScaleGraphB, &QPushButton::clicked, this, &Ekos::Align::slotAutoScaleGraph); connect(mountModelB, &QPushButton::clicked, this, &Ekos::Align::slotMountModel); connect(solutionTable, &QTableWidget::cellClicked, this, &Ekos::Align::selectSolutionTableRow); connect(mountModel.wizardAlignB, &QPushButton::clicked, this, &Ekos::Align::slotWizardAlignmentPoints); connect(mountModel.alignTypeBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::alignTypeChanged); connect(mountModel.starListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.greekStarListBox, static_cast(&QComboBox::currentIndexChanged), this, &Ekos::Align::slotStarSelected); connect(mountModel.loadAlignB, &QPushButton::clicked, this, &Ekos::Align::slotLoadAlignmentPoints); connect(mountModel.saveAsAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAsAlignmentPoints); connect(mountModel.saveAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSaveAlignmentPoints); connect(mountModel.clearAllAlignB, &QPushButton::clicked, this, &Ekos::Align::slotClearAllAlignPoints); connect(mountModel.removeAlignB, &QPushButton::clicked, this, &Ekos::Align::slotRemoveAlignPoint); connect(mountModel.addAlignB, &QPushButton::clicked, this, &Ekos::Align::slotAddAlignPoint); connect(mountModel.findAlignB, &QPushButton::clicked, this, &Ekos::Align::slotFindAlignObject); connect(mountModel.sortAlignB, &QPushButton::clicked, this, &Ekos::Align::slotSortAlignmentPoints); connect(mountModel.previewB, &QPushButton::clicked, this, &Ekos::Align::togglePreviewAlignPoints); connect(mountModel.stopAlignB, &QPushButton::clicked, this, &Ekos::Align::resetAlignmentProcedure); connect(mountModel.startAlignB, &QPushButton::clicked, this, &Ekos::Align::startStopAlignmentProcedure); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); } Align::~Align() { if (alignWidget->parent() == nullptr) toggleAlignWidgetFullScreen(); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); } void Align::selectSolutionTableRow(int row, int column) { Q_UNUSED(column); solutionTable->selectRow(row); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { if (i == row) { item->setColor(Qt::black); item->setBrush(Qt::yellow); } else { item->setColor(Qt::red); item->setBrush(Qt::white); } } } } alignPlot->replot(); } void Align::handleHorizontalPlotSizeChange() { alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::handleVerticalPlotSizeChange() { alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); alignPlot->replot(); } void Align::resizeEvent(QResizeEvent *event) { if (event->oldSize().width() != -1) { if (event->oldSize().width() != size().width()) handleHorizontalPlotSizeChange(); else if (event->oldSize().height() != size().height()) handleVerticalPlotSizeChange(); } else { QTimer::singleShot(10, this, &Ekos::Align::handleHorizontalPlotSizeChange); } } void Align::handlePointTooltip(QMouseEvent *event) { QCPAbstractItem *item = alignPlot->itemAt(event->localPos()); if (item) { QCPItemText *label = qobject_cast(item); if (label) { QString labelText = label->text(); int point = labelText.toInt() - 1; if (point < 0) return; QToolTip::showText(event->globalPos(), tr("" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
Object %L1: %L2
RA:%L3
DE:%L4
dRA:%L5
dDE:%L6
") .arg(point + 1) .arg(solutionTable->item(point, 2)->text(), solutionTable->item(point, 0)->text(), solutionTable->item(point, 1)->text(), solutionTable->item(point, 4)->text(), solutionTable->item(point, 5)->text()), alignPlot, alignPlot->rect()); } } } void Align::buildTarget() { double accuracyRadius = accuracySpin->value(); if (centralTarget) { concentricRings->data()->clear(); redTarget->data()->clear(); yellowTarget->data()->clear(); centralTarget->data()->clear(); } else { concentricRings = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); redTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); yellowTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); centralTarget = new QCPCurve(alignPlot->xAxis, alignPlot->yAxis); } const int pointCount = 200; QVector circleRings( pointCount * (5)); //Have to multiply by the number of rings, Rings at : 25%, 50%, 75%, 125%, 175% QVector circleCentral(pointCount); QVector circleYellow(pointCount); QVector circleRed(pointCount); int circleRingPt = 0; for (int i = 0; i < pointCount; i++) { double theta = i / static_cast(pointCount) * 2 * M_PI; for (double ring = 1; ring < 8; ring++) { if (ring != 4 && ring != 6) { if (i % (9 - static_cast(ring)) == 0) //This causes fewer points to draw on the inner circles. { circleRings[circleRingPt] = QCPCurveData(circleRingPt, accuracyRadius * ring * 0.25 * qCos(theta), accuracyRadius * ring * 0.25 * qSin(theta)); circleRingPt++; } } } circleCentral[i] = QCPCurveData(i, accuracyRadius * qCos(theta), accuracyRadius * qSin(theta)); circleYellow[i] = QCPCurveData(i, accuracyRadius * 1.5 * qCos(theta), accuracyRadius * 1.5 * qSin(theta)); circleRed[i] = QCPCurveData(i, accuracyRadius * 2 * qCos(theta), accuracyRadius * 2 * qSin(theta)); } concentricRings->setLineStyle(QCPCurve::lsNone); concentricRings->setScatterSkip(0); concentricRings->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, QColor(255, 255, 255, 150), 1)); concentricRings->data()->set(circleRings, true); redTarget->data()->set(circleRed, true); yellowTarget->data()->set(circleYellow, true); centralTarget->data()->set(circleCentral, true); concentricRings->setPen(QPen(Qt::white)); redTarget->setPen(QPen(Qt::red)); yellowTarget->setPen(QPen(Qt::yellow)); centralTarget->setPen(QPen(Qt::green)); concentricRings->setBrush(Qt::NoBrush); redTarget->setBrush(QBrush(QColor(255, 0, 0, 50))); yellowTarget->setBrush( QBrush(QColor(0, 255, 0, 50))); //Note this is actually yellow. It is green on top of red with equal opacity. centralTarget->setBrush(QBrush(QColor(0, 255, 0, 50))); if (alignPlot->size().width() > 0) alignPlot->replot(); } void Align::slotAutoScaleGraph() { double accuracyRadius = accuracySpin->value(); alignPlot->xAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->yAxis->setRange(-accuracyRadius * 3, accuracyRadius * 3); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); alignPlot->replot(); } void Align::slotWizardAlignmentPoints() { int points = mountModel.alignPtNum->value(); if (points < 2) //The minimum is 2 because the wizard calculations require the calculation of an angle between points. return; //It should not be less than 2 because the minimum in the spin box is 2. int minAlt = mountModel.minAltBox->value(); KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double lat = geo->lat()->Degrees(); if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { double decAngle = mountModel.alignDec->value(); //Dec that never rises. if (lat > 0) { if (decAngle < lat - 90 + minAlt) //Min altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } else { if (decAngle > lat + 90 - minAlt) //Max altitude possible at minAlt deg above horizon { KSNotification::sorry(i18n("DEC is below the altitude limit")); return; } } } //If there are less than 6 points, keep them all in the same DEC, //any more, set the num per row to be the sqrt of the points to evenly distribute in RA and DEC int numRAperDEC = 5; if (points > 5) numRAperDEC = qSqrt(points); //These calculations rely on modulus and int division counting beginning at 0, but the #s start at 1. int decPoints = (points - 1) / numRAperDEC + 1; int lastSetRAPoints = (points - 1) % numRAperDEC + 1; double decIncrement = -1; double initDEC = -1; SkyPoint spTest; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { decPoints = 1; initDEC = mountModel.alignDec->value(); decIncrement = 0; } else if (decPoints == 1) { decIncrement = 0; spTest.setAlt( minAlt); //The goal here is to get the point exactly West at the minAlt so that we can use that DEC spTest.setAz(270); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); } else { spTest.setAlt( minAlt + 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. spTest.setAz(180); spTest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); initDEC = spTest.dec().Degrees(); if (lat > 0) decIncrement = (80 - initDEC) / (decPoints); //Don't quite want to reach NCP else decIncrement = (initDEC - 80) / (decPoints); //Don't quite want to reach SCP } for (int d = 0; d < decPoints; d++) { double initRA = -1; double raPoints = -1; double raIncrement = -1; double dec; if (lat > 0) dec = initDEC + d * decIncrement; else dec = initDEC - d * decIncrement; if (mountModel.alignTypeBox->currentIndex() == OBJECT_FIXED_DEC) { raPoints = points; } else if (d == decPoints - 1) { raPoints = lastSetRAPoints; } else { raPoints = numRAperDEC; } //This computes both the initRA and the raIncrement. calculateAngleForRALine(raIncrement, initRA, dec, lat, raPoints, minAlt); if (raIncrement == -1 || decIncrement == -1) { KSNotification::sorry(i18n("Point calculation error.")); return; } for (int i = 0; i < raPoints; i++) { double ra = initRA + i * raIncrement; const SkyObject *original = getWizardAlignObject(ra, dec); QString ra_report, dec_report, name; if (original) { SkyObject *o = original->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); name = o->longname(); } else { getFormattedCoords(dms(ra).Hours(), dec, ra_report, dec_report); name = i18n("Sky Point"); } int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::calculateAngleForRALine(double &raIncrement, double &initRA, double initDEC, double lat, double raPoints, double minAlt) { SkyPoint spEast; SkyPoint spWest; //Circumpolar dec if (fabs(initDEC) > (90 - fabs(lat) + minAlt)) { if (raPoints > 1) raIncrement = 360 / (raPoints - 1); else raIncrement = 0; initRA = 0; } else { dms AZEast, AZWest; calculateAZPointsForDEC(dms(initDEC), dms(minAlt), AZEast, AZWest); spEast.setAlt(minAlt); spEast.setAz(AZEast.Degrees()); spEast.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); spWest.setAlt(minAlt); spWest.setAz(AZWest.Degrees()); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); dms angleSep = spEast.ra().deltaAngle(spWest.ra()); //dms angleSep; // if (spEast.ra().Degrees() > spWest.ra().Degrees()) // angleSep = spEast.ra() - spWest.ra(); // else // angleSep = spEast.ra() + dms(360) - spWest.ra(); initRA = spWest.ra().Degrees(); if (raPoints > 1) raIncrement = fabs(angleSep.Degrees() / (raPoints - 1)); else raIncrement = 0; } } void Align::calculateAZPointsForDEC(dms dec, dms alt, dms &AZEast, dms &AZWest) { KStarsData *data = KStarsData::Instance(); GeoLocation *geo = data->geo(); double AZRad; double sindec, cosdec, sinlat, coslat; double sinAlt, cosAlt; geo->lat()->SinCos(sinlat, coslat); dec.SinCos(sindec, cosdec); alt.SinCos(sinAlt, cosAlt); double arg = (sindec - sinlat * sinAlt) / (coslat * cosAlt); AZRad = acos(arg); AZEast.setRadians(AZRad); AZWest.setRadians(2.0 * dms::PI - AZRad); } const SkyObject *Align::getWizardAlignObject(double ra, double dec) { double maxSearch = 5.0; switch (mountModel.alignTypeBox->currentIndex()) { case OBJECT_ANY_OBJECT: return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); case OBJECT_FIXED_DEC: case OBJECT_FIXED_GRID: return nullptr; case OBJECT_ANY_STAR: return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); } // if (mountModel.alignTypeBox->currentText() == "Any Object") // return KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); // else if (mountModel.alignTypeBox->currentText() == "Fixed DEC" || // mountModel.alignTypeBox->currentText() == "Fixed Grid") // return nullptr; // else if (mountModel.alignTypeBox->currentText() == "Any Stars") // return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); //If they want named stars, then try to search for and return the closest Align Star to the requested location dms bestDiff = dms(360); double index = -1; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->hasName()) { SkyPoint thisPt(ra / 15.0, dec); dms thisDiff = thisPt.angularDistanceTo(star); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } } if (index == -1) return KStarsData::Instance()->skyComposite()->starNearest(new SkyPoint(dms(ra), dms(dec)), maxSearch); return alignStars.value(index); } void Align::alignTypeChanged(int alignType) { if (alignType == OBJECT_FIXED_DEC) mountModel.alignDec->setEnabled(true); else mountModel.alignDec->setEnabled(false); } void Align::slotStarSelected(const QString selectedStar) { for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (star->name() == selectedStar || star->gname().simplified() == selectedStar) { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(star->ra0().Hours(), star->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(star->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); mountModel.starListBox->setCurrentIndex(0); mountModel.greekStarListBox->setCurrentIndex(0); return; } } } if (previewShowing) updatePreviewAlignPoints(); } void Align::generateAlignStarList() { alignStars.clear(); mountModel.starListBox->clear(); mountModel.greekStarListBox->clear(); KStarsData *data = KStarsData::Instance(); QVector> listStars; listStars.append(data->skyComposite()->objectLists(SkyObject::STAR)); for (int i = 0; i < listStars.size(); i++) { QPair pair = listStars.value(i); const StarObject *star = dynamic_cast(pair.second); if (star) { StarObject *alignStar = star->clone(); alignStar->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); alignStars.append(alignStar); } } QStringList boxNames; QStringList greekBoxNames; for (int i = 0; i < alignStars.size(); i++) { const StarObject *star = alignStars.value(i); if (star) { if (!isVisible(star)) { alignStars.remove(i); i--; } else { if (star->hasLatinName()) boxNames << star->name(); else { if (!star->gname().isEmpty()) greekBoxNames << star->gname().simplified(); } } } } boxNames.sort(Qt::CaseInsensitive); boxNames.removeDuplicates(); greekBoxNames.removeDuplicates(); qSort(greekBoxNames.begin(), greekBoxNames.end(), [](const QString & a, const QString & b) { QStringList aParts = a.split(' '); QStringList bParts = b.split(' '); if (aParts.length() < 2 || bParts.length() < 2) return a < b; //This should not happen, they should all have 2 words in the string. if (aParts[1] == bParts[1]) { return aParts[0] < bParts[0]; //This compares the greek letter when the constellation is the same } else return aParts[1] < bParts[1]; //This compares the constellation names }); mountModel.starListBox->addItem("Select one:"); mountModel.greekStarListBox->addItem("Select one:"); for (int i = 0; i < boxNames.size(); i++) mountModel.starListBox->addItem(boxNames.at(i)); for (int i = 0; i < greekBoxNames.size(); i++) mountModel.greekStarListBox->addItem(greekBoxNames.at(i)); } bool Align::isVisible(const SkyObject *so) { return (getAltitude(so) > 30); } double Align::getAltitude(const SkyObject *so) { KStarsData *data = KStarsData::Instance(); SkyPoint sp = so->recomputeCoords(data->ut(), data->geo()); //check altitude of object at this time. sp.EquatorialToHorizontal(data->lst(), data->geo()->lat()); return sp.alt().Degrees(); } void Align::togglePreviewAlignPoints() { previewShowing = !previewShowing; mountModel.previewB->setChecked(previewShowing); updatePreviewAlignPoints(); } void Align::updatePreviewAlignPoints() { FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags(); for (int i = 0; i < flags->size(); i++) { if (flags->label(i).startsWith(QLatin1String("Align"))) { flags->remove(i); i--; } } if (previewShowing) { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (raCell && deCell && objNameCell) { QString raString = raCell->text(); QString deString = deCell->text(); dms raDMS = dms::fromString(raString, false); dms decDMS = dms::fromString(deString, true); QString objString = objNameCell->text(); SkyPoint flagPoint(raDMS, decDMS); flags->add(flagPoint, "J2000", "Default", "Align " + QString::number(i + 1) + ' ' + objString, "white"); } } } KStars::Instance()->map()->forceUpdate(true); } void Align::slotLoadAlignmentPoints() { QUrl fileURL = QFileDialog::getOpenFileUrl(&mountModelDialog, i18n("Open Ekos Alignment List"), alignURLPath, "Ekos AlignmentList (*.eal)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } alignURLPath = QUrl(fileURL.url(QUrl::RemoveFilename)); loadAlignmentPoints(fileURL.toLocalFile()); if (previewShowing) updatePreviewAlignPoints(); } bool Align::loadAlignmentPoints(const QString &fileURL) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } mountModel.alignTable->setRowCount(0); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { double sqVersion = atof(findXMLAttValu(root, "version")); if (sqVersion < AL_FORMAT_VERSION) { appendLogText(i18n("Deprecated sequence file format version %1. Please construct a new sequence file.", sqVersion)); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; int currentRow = 0; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "AlignmentPoint")) { mountModel.alignTable->insertRow(currentRow); subEP = findXMLEle(ep, "RA"); if (subEP) { QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(pcdataXMLEle(subEP)); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); } else return false; subEP = findXMLEle(ep, "DE"); if (subEP) { QTableWidgetItem *DEReport = new QTableWidgetItem(); DEReport->setText(pcdataXMLEle(subEP)); DEReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DEReport); } else return false; subEP = findXMLEle(ep, "NAME"); if (subEP) { QTableWidgetItem *ObjReport = new QTableWidgetItem(); ObjReport->setText(pcdataXMLEle(subEP)); ObjReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjReport); } else return false; } currentRow++; } return true; } } return false; } void Align::slotSaveAsAlignmentPoints() { alignURL.clear(); slotSaveAlignmentPoints(); } void Align::slotSaveAlignmentPoints() { QUrl backupCurrent = alignURL; if (alignURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || alignURL.toLocalFile().contains("/Temp")) alignURL.clear(); if (alignURL.isEmpty()) { alignURL = QFileDialog::getSaveFileUrl(&mountModelDialog, i18n("Save Ekos Alignment List"), alignURLPath, "Ekos Alignment List (*.eal)"); // if user presses cancel if (alignURL.isEmpty()) { alignURL = backupCurrent; return; } alignURLPath = QUrl(alignURL.url(QUrl::RemoveFilename)); if (alignURL.toLocalFile().endsWith(QLatin1String(".eal")) == false) alignURL.setPath(alignURL.toLocalFile() + ".eal"); if (QFile::exists(alignURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", alignURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } } if (alignURL.isValid()) { if ((saveAlignmentPoints(alignURL.toLocalFile())) == false) { KSNotification::error(i18n("Failed to save alignment list"), i18n("Save")); return; } } else { QString message = i18n("Invalid URL: %1", alignURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Align::saveAlignmentPoints(const QString &path) { QFile file; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); outstream << "" << endl; outstream << "" << endl; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); QTableWidgetItem *objNameCell = mountModel.alignTable->item(i, 2); if (!raCell || !deCell || !objNameCell) return false; QString raString = raCell->text(); QString deString = deCell->text(); QString objString = objNameCell->text(); outstream << "" << endl; outstream << "" << raString << "" << endl; outstream << "" << deString << "" << endl; outstream << "" << objString << "" << endl; outstream << "" << endl; } outstream << "" << endl; appendLogText(i18n("Alignment List saved to %1", path)); file.close(); return true; } void Align::slotSortAlignmentPoints() { int firstAlignmentPt = findClosestAlignmentPointToTelescope(); if (firstAlignmentPt != -1) { swapAlignPoints(firstAlignmentPt, 0); } for (int i = 0; i < mountModel.alignTable->rowCount() - 1; i++) { int nextAlignmentPoint = findNextAlignmentPointAfter(i); if (nextAlignmentPoint != -1) { swapAlignPoints(nextAlignmentPoint, i + 1); } } if (previewShowing) updatePreviewAlignPoints(); } int Align::findClosestAlignmentPointToTelescope() { dms bestDiff = dms(360); double index = -1; for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); dms thisDiff = telescopeCoord.angularDistanceTo(new SkyPoint(raDMS, deDMS)); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } int Align::findNextAlignmentPointAfter(int currentSpot) { QTableWidgetItem *currentRACell = mountModel.alignTable->item(currentSpot, 0); QTableWidgetItem *currentDECell = mountModel.alignTable->item(currentSpot, 1); if (currentRACell && currentDECell) { dms thisRADMS = dms::fromString(currentRACell->text(), false); dms thisDEDMS = dms::fromString(currentDECell->text(), true); SkyPoint thisPt(thisRADMS, thisDEDMS); dms bestDiff = dms(360); double index = -1; for (int i = currentSpot + 1; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); QTableWidgetItem *deCell = mountModel.alignTable->item(i, 1); if (raCell && deCell) { dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); SkyPoint point(raDMS, deDMS); dms thisDiff = thisPt.angularDistanceTo(&point); if (thisDiff.Degrees() < bestDiff.Degrees()) { index = i; bestDiff = thisDiff; } } } return index; } else return -1; } void Align::exportSolutionPoints() { if (solutionTable->rowCount() == 0) return; QUrl exportFile = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Solution Points"), alignURLPath, "CSV File (*.csv)"); if (exportFile.isEmpty()) // if user presses cancel return; if (exportFile.toLocalFile().endsWith(QLatin1String(".csv")) == false) exportFile.setPath(exportFile.toLocalFile() + ".csv"); QString path = exportFile.toLocalFile(); if (QFile::exists(path)) { int r = KMessageBox::warningContinueCancel(nullptr, i18n("A file named \"%1\" already exists. " "Overwrite it?", exportFile.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } if (!exportFile.isValid()) { QString message = i18n("Invalid URL: %1", exportFile.url()); KSNotification::sorry(message, i18n("Invalid URL")); return; } QFile file; file.setFileName(path); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", path); KSNotification::sorry(message, i18n("Could Not Open File")); return; } QTextStream outstream(&file); QString epoch = QString::number(KStarsDateTime::currentDateTime().epoch()); outstream << "RA (J" << epoch << "),DE (J" << epoch << "),RA (degrees),DE (degrees),Name,RA Error (arcsec),DE Error (arcsec)" << endl; for (int i = 0; i < solutionTable->rowCount(); i++) { QTableWidgetItem *raCell = solutionTable->item(i, 0); QTableWidgetItem *deCell = solutionTable->item(i, 1); QTableWidgetItem *objNameCell = solutionTable->item(i, 2); QTableWidgetItem *raErrorCell = solutionTable->item(i, 4); QTableWidgetItem *deErrorCell = solutionTable->item(i, 5); if (!raCell || !deCell || !objNameCell || !raErrorCell || !deErrorCell) { KSNotification::sorry(i18n("Error in table structure.")); return; } dms raDMS = dms::fromString(raCell->text(), false); dms deDMS = dms::fromString(deCell->text(), true); outstream << raDMS.toHMSString() << ',' << deDMS.toDMSString() << ',' << raDMS.Degrees() << ',' << deDMS.Degrees() << ',' << objNameCell->text() << ',' << raErrorCell->text().remove('\"') << ',' << deErrorCell->text().remove('\"') << endl; } appendLogText(i18n("Solution Points Saved as: %1", path)); file.close(); } void Align::slotClearAllSolutionPoints() { if (solutionTable->rowCount() == 0) return; // if (Options::autonomousMode() || KMessageBox::questionYesNo( // KStars::Instance(), i18n("Are you sure you want to clear all of the solution points?"), // i18n("Clear Solution Points"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) // { // solutionTable->setRowCount(0); // alignPlot->graph(0)->data()->clear(); // alignPlot->clearItems(); // buildTarget(); // slotAutoScaleGraph(); // } connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); solutionTable->setRowCount(0); alignPlot->graph(0)->data()->clear(); alignPlot->clearItems(); buildTarget(); slotAutoScaleGraph(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to clear all of the solution points?"), i18n("Clear Solution Points"), 60); } void Align::slotClearAllAlignPoints() { if (mountModel.alignTable->rowCount() == 0) return; if (KMessageBox::questionYesNo(&mountModelDialog, i18n("Are you sure you want to clear all the alignment points?"), i18n("Clear Align Points")) == KMessageBox::Yes) mountModel.alignTable->setRowCount(0); if (previewShowing) updatePreviewAlignPoints(); } void Align::slotRemoveSolutionPoint() { QCPAbstractItem *abstractItem = alignPlot->item(solutionTable->currentRow()); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) { double point = item->position->key(); alignPlot->graph(0)->data()->remove(point); } } alignPlot->removeItem(solutionTable->currentRow()); for (int i = 0; i < alignPlot->itemCount(); i++) { QCPAbstractItem *abstractItem = alignPlot->item(i); if (abstractItem) { QCPItemText *item = qobject_cast(abstractItem); if (item) item->setText(QString::number(i + 1)); } } solutionTable->removeRow(solutionTable->currentRow()); alignPlot->replot(); } void Align::slotRemoveAlignPoint() { mountModel.alignTable->removeRow(mountModel.alignTable->currentRow()); if (previewShowing) updatePreviewAlignPoints(); } void Align::moveAlignPoint(int logicalIndex, int oldVisualIndex, int newVisualIndex) { Q_UNUSED(logicalIndex); for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *oldItem = mountModel.alignTable->takeItem(oldVisualIndex, i); QTableWidgetItem *newItem = mountModel.alignTable->takeItem(newVisualIndex, i); mountModel.alignTable->setItem(newVisualIndex, i, oldItem); mountModel.alignTable->setItem(oldVisualIndex, i, newItem); } mountModel.alignTable->verticalHeader()->blockSignals(true); mountModel.alignTable->verticalHeader()->moveSection(newVisualIndex, oldVisualIndex); mountModel.alignTable->verticalHeader()->blockSignals(false); if (previewShowing) updatePreviewAlignPoints(); } void Align::swapAlignPoints(int firstPt, int secondPt) { for (int i = 0; i < mountModel.alignTable->columnCount(); i++) { QTableWidgetItem *firstPtItem = mountModel.alignTable->takeItem(firstPt, i); QTableWidgetItem *secondPtItem = mountModel.alignTable->takeItem(secondPt, i); mountModel.alignTable->setItem(firstPt, i, secondPtItem); mountModel.alignTable->setItem(secondPt, i, firstPtItem); } } void Align::slotMountModel() { generateAlignStarList(); SkyPoint spWest; spWest.setAlt(30); spWest.setAz(270); spWest.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); mountModel.alignDec->setValue(static_cast(spWest.dec().Degrees())); mountModelDialog.show(); } void Align::slotAddAlignPoint() { int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } void Align::slotFindAlignObject() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); int currentRow = mountModel.alignTable->rowCount(); mountModel.alignTable->insertRow(currentRow); QString ra_report, dec_report; getFormattedCoords(o->ra0().Hours(), o->dec0().Degrees(), ra_report, dec_report); QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ra_report); RAReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(dec_report); DECReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 1, DECReport); QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(o->longname()); ObjNameReport->setTextAlignment(Qt::AlignHCenter); mountModel.alignTable->setItem(currentRow, 2, ObjNameReport); QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); mountModel.alignTable->setItem(currentRow, 3, disabledBox); } } if (previewShowing) updatePreviewAlignPoints(); } void Align::resetAlignmentProcedure() { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); appendLogText(i18n("The Mount Model Tool is Reset.")); mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModelRunning = false; currentAlignmentPoint = 0; abort(); } bool Align::alignmentPointsAreBad() { for (int i = 0; i < mountModel.alignTable->rowCount(); i++) { QTableWidgetItem *raCell = mountModel.alignTable->item(i, 0); if (!raCell) return true; QString raString = raCell->text(); if (dms().setFromString(raString, false) == false) return true; QTableWidgetItem *decCell = mountModel.alignTable->item(i, 1); if (!decCell) return true; QString decString = decCell->text(); if (dms().setFromString(decString, true) == false) return true; } return false; } void Align::startStopAlignmentProcedure() { if (!mountModelRunning) { if (mountModel.alignTable->rowCount() > 0) { if (alignmentPointsAreBad()) { KSNotification::error(i18n("Please Check the Alignment Points.")); return; } if (currentGotoMode == GOTO_NOTHING) { int r = KMessageBox::warningContinueCancel( nullptr, i18n("In the Align Module, \"Nothing\" is Selected for the Solver Action. This means that the " "mount model tool will not sync/align your mount but will only report the pointing model " "errors. Do you wish to continue?"), i18n("Pointing Model Report Only?"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "nothing_selected_warning"); if (r == KMessageBox::Cancel) return; } if (currentAlignmentPoint == 0) { for (int row = 0; row < mountModel.alignTable->rowCount(); row++) { QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon()); mountModel.alignTable->setItem(row, 3, statusReport); } } mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-pause")); mountModelRunning = true; appendLogText(i18n("The Mount Model Tool is Starting.")); startAlignmentPoint(); } } else { mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); appendLogText(i18n("The Mount Model Tool is Paused.")); abort(); mountModelRunning = false; QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); } } void Align::startAlignmentPoint() { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { QTableWidgetItem *raCell = mountModel.alignTable->item(currentAlignmentPoint, 0); QString raString = raCell->text(); dms raDMS = dms::fromString(raString, false); double ra = raDMS.Hours(); QTableWidgetItem *decCell = mountModel.alignTable->item(currentAlignmentPoint, 1); QString decString = decCell->text(); dms decDMS = dms::fromString(decString, true); double dec = decDMS.Degrees(); QProgressIndicator *alignIndicator = new QProgressIndicator(this); mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, alignIndicator); alignIndicator->startAnimation(); targetCoord.setRA0(ra); targetCoord.setDec0(dec); targetCoord.updateCoordsNow(KStarsData::Instance()->updateNum()); Slew(); } } void Align::finishAlignmentPoint(bool solverSucceeded) { if (mountModelRunning && currentAlignmentPoint >= 0 && currentAlignmentPoint < mountModel.alignTable->rowCount()) { mountModel.alignTable->setCellWidget(currentAlignmentPoint, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setFlags(Qt::ItemIsSelectable); if (solverSucceeded) statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); else statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); mountModel.alignTable->setItem(currentAlignmentPoint, 3, statusReport); currentAlignmentPoint++; if (currentAlignmentPoint < mountModel.alignTable->rowCount()) { startAlignmentPoint(); } else { mountModelRunning = false; mountModel.startAlignB->setIcon( QIcon::fromTheme("media-playback-start")); appendLogText(i18n("The Mount Model Tool is Finished.")); currentAlignmentPoint = 0; } } } bool Align::isParserOK() { bool rc = parser->init(); if (rc) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } return rc; } void Align::checkAlignmentTimeout() { if (loadSlewState != IPS_IDLE || ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) abort(); else if (loadSlewState == IPS_IDLE) { appendLogText(i18n("Solver timed out.")); parser->stopSolver(); captureAndSolve(); } // TODO must also account for loadAndSlew. Retain file name } void Align::setSolverType(int type) { if (sender() == nullptr && type >= 0 && type <= 2) solverTypeGroup->button(type)->setChecked(true); syncSettings(); Options::setSolverType(type); switch (type) { case SOLVER_ONLINE: loadSlewB->setEnabled(true); if (onlineParser.get() != nullptr) { parser = onlineParser.get(); return; } onlineParser.reset(new Ekos::OnlineAstrometryParser()); parser = onlineParser.get(); break; case SOLVER_OFFLINE: loadSlewB->setEnabled(true); if (offlineParser.get() != nullptr) { parser = offlineParser.get(); return; } offlineParser.reset(new Ekos::OfflineAstrometryParser()); parser = offlineParser.get(); break; case SOLVER_REMOTE: loadSlewB->setEnabled(true); if (remoteParser.get() != nullptr && remoteParserDevice != nullptr) { parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); return; } remoteParser.reset(new Ekos::RemoteAstrometryParser()); parser = remoteParser.get(); (dynamic_cast(parser))->setAstrometryDevice(remoteParserDevice); if (currentCCD) (dynamic_cast(parser))->setCCD(currentCCD->getDeviceName()); break; } parser->setAlign(this); if (parser->init()) { connect(parser, &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(parser, &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } else parser->disconnect(); } bool Align::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Align::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Align::setDefaultCCD(QString ccd) { syncSettings(); Options::setDefaultAlignCCD(ccd); } void Align::checkCCD(int ccdNum) { if (ccdNum == -1 || ccdNum >= CCDs.count()) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) (dynamic_cast(remoteParser.get()))->setCCD(currentCCD->getDeviceName()); syncCCDInfo(); /* FOVScopeCombo->blockSignals(true); ISD::CCD::TelescopeType type = currentCCD->getTelescopeType(); FOVScopeCombo->setCurrentIndex(type == ISD::CCD::TELESCOPE_UNKNOWN ? 0 : type); FOVScopeCombo->blockSignals(false); */ syncTelescopeInfo(); } void Align::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) { syncCCDInfo(); return; } CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); syncSettings(); } void Align::setTelescope(ISD::GDInterface *newTelescope) { currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); connect(currentTelescope, &ISD::GDInterface::Disconnected, this, [this]() { m_isRateSynced = false; }); if (m_isRateSynced == false) { PAHSlewRateCombo->blockSignals(true); PAHSlewRateCombo->clear(); PAHSlewRateCombo->addItems(currentTelescope->slewRates()); if (Options::pAHMountSpeedIndex() >= 0) PAHSlewRateCombo->setCurrentIndex(Options::pAHMountSpeedIndex()); else PAHSlewRateCombo->setCurrentIndex(currentTelescope->getSlewRate()); PAHSlewRateCombo->blockSignals(false); m_isRateSynced = !currentTelescope->slewRates().empty(); } syncTelescopeInfo(); } void Align::setDome(ISD::GDInterface *newDome) { currentDome = static_cast(newDome); connect(currentDome, &ISD::GDInterface::switchUpdated, this, &Ekos::Align::processSwitch, Qt::UniqueConnection); } void Align::removeDevice(ISD::GDInterface *device) { device->disconnect(this); if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope = nullptr; m_isRateSynced = false; } else if (currentDome && !strcmp(currentDome->getDeviceName(), device->getDeviceName())) { currentDome = nullptr; } else if (currentRotator && !strcmp(currentRotator->getDeviceName(), device->getDeviceName())) { currentRotator = nullptr; } if (CCDs.contains(static_cast(device))) { CCDs.removeAll(static_cast(device)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(device->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) currentCCD = nullptr; checkCCD(); } if (Filters.contains(static_cast(device))) { Filters.removeAll(static_cast(device)); filterManager->removeDevice(device); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(device->getDeviceName())); if (Filters.empty()) currentFilter = nullptr; checkFilter(); } } bool Align::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; canSync = currentTelescope->canSync(); if (canSync == false && syncR->isEnabled()) { slewR->setChecked(true); appendLogText(i18n("Mount does not support syncing.")); } syncR->setEnabled(canSync); INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryAperture = np->value; np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideAperture = np->value; aperture = primaryAperture; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryFL = np->value; np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideFL = np->value; focal_length = primaryFL; //if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) if (FOVScopeCombo->currentIndex() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; } if (focal_length == -1 || aperture == -1) return false; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_PRIMARY, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(primaryFL / primaryAperture, 'f', 1), QString::number(primaryFL, 'f', 2), QString::number(primaryAperture, 'f', 2)), Qt::ToolTipRole); FOVScopeCombo->setItemData( ISD::CCD::TELESCOPE_GUIDE, i18nc("F-Number, Focal Length, Aperture", "F%1 Focal Length: %2 mm Aperture: %3 mm2", QString::number(guideFL / guideAperture, 'f', 1), QString::number(guideFL, 'f', 2), QString::number(guideAperture, 'f', 2)), Qt::ToolTipRole); calculateFOV(); generateArgs(); return true; } return false; } void Align::setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture) { if (primaryFocalLength > 0) primaryFL = primaryFocalLength; if (guideFocalLength > 0) guideFL = guideFocalLength; if (primaryAperture > 0) this->primaryAperture = primaryAperture; if (guideAperture > 0) this->guideAperture = guideAperture; focal_length = primaryFL; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) focal_length = guideFL; aperture = primaryAperture; if (currentCCD && currentCCD->getTelescopeType() == ISD::CCD::TELESCOPE_GUIDE) aperture = guideAperture; syncTelescopeInfo(); } void Align::syncCCDInfo() { INumberVectorProperty *nvp = nullptr; if (currentCCD == nullptr) return; if (useGuideHead) nvp = currentCCD->getBaseDevice()->getNumber("GUIDER_INFO"); else nvp = currentCCD->getBaseDevice()->getNumber("CCD_INFO"); if (nvp) { INumber *np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_X"); if (np && np->value > 0) ccd_hor_pixel = ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; np = IUFindNumber(nvp, "CCD_PIXEL_SIZE_Y"); if (np && np->value > 0) ccd_ver_pixel = np->value; } ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); ISwitchVectorProperty *svp = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); if (svp) setWCSEnabled(Options::astrometrySolverWCS()); targetChip->setImageView(alignView, FITS_ALIGN); targetChip->getFrameMinMax(nullptr, nullptr, nullptr, nullptr, nullptr, &ccd_width, nullptr, &ccd_height); //targetChip->getFrame(&x,&y,&ccd_width,&ccd_height); binningCombo->setEnabled(targetChip->canBin()); if (targetChip->canBin()) { binningCombo->blockSignals(true); int binx = 1, biny = 1; targetChip->getMaxBin(&binx, &biny); binningCombo->clear(); for (int i = 0; i < binx; i++) binningCombo->addItem(QString("%1x%2").arg(i + 1).arg(i + 1)); // By default, set to maximum binning since the solver behaves better this way // solverBinningIndex is set by default to 4, but as soon as the user changes the binning, it changes // to whatever value the user selected. if (Options::solverBinningIndex() == 4 && binningCombo->count() <= 4) { binningCombo->setCurrentIndex(binningCombo->count() - 1); Options::setSolverBinningIndex(binningCombo->count() - 1); } else binningCombo->setCurrentIndex(Options::solverBinningIndex()); binningCombo->blockSignals(false); } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) return; if (ccd_hor_pixel != -1 && ccd_ver_pixel != -1 && focal_length != -1 && aperture != -1) { calculateFOV(); generateArgs(); } } void Align::getFOVScale(double &fov_w, double &fov_h, double &fov_scale) { fov_w = fov_x; fov_h = fov_y; fov_scale = fov_pixscale; } QList Align::fov() { QList result; result << fov_x << fov_y << fov_pixscale; return result; } void Align::getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale) { // FOV in arcsecs fov_w = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_h = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_scale = (fov_w * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_w /= 60.0; fov_h /= 60.0; } void Align::calculateFOV() { // Calculate FOV // FOV in arcsecs fov_x = 206264.8062470963552 * ccd_width * ccd_hor_pixel / 1000.0 / focal_length; fov_y = 206264.8062470963552 * ccd_height * ccd_ver_pixel / 1000.0 / focal_length; // Pix Scale fov_pixscale = (fov_x * (Options::solverBinningIndex() + 1)) / ccd_width; // FOV in arcmins fov_x /= 60.0; fov_y /= 60.0; QString calculatedFOV = (QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); // JM 2018-04-20 Above calculations are for RAW FOV. Starting from 2.9.5, we are using EFFECTIVE FOV // Which is the real FOV as measured from the plate solution. The effective FOVs are stored in the database and are unique // per profile/pixel_size/focal_length combinations. It defaults to 0' x 0' and gets updated after the first successful solver is complete. getEffectiveFOV(); if (fov_x == 0) { //FOVOut->setReadOnly(false); FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

Please capture and solve once to measure the effective FOV or enter the values manually.

Calculated FOV: %1

", calculatedFOV)); } else { FOVOut->setToolTip(i18n("

Effective field of view size in arcminutes.

")); //FOVOut->setReadOnly(true); } solverFOV->setSize(fov_x, fov_y); sensorFOV->setSize(fov_x, fov_y); if (currentCCD) sensorFOV->setName(currentCCD->getDeviceName()); FOVOut->setText(QString("%1' x %2'").arg(fov_x, 0, 'g', 3).arg(fov_y, 0, 'g', 3)); if (((fov_x + fov_y) / 2.0) > PAH_CUTOFF_FOV) { if (isPAHReady == false) { PAHWidgets->setEnabled(true); isPAHReady = true; emit PAHEnabled(true); PAHWidgets->setToolTip(QString()); FOVDisabledLabel->hide(); } } else if (PAHWidgets->isEnabled()) { PAHWidgets->setEnabled(false); isPAHReady = false; emit PAHEnabled(false); PAHWidgets->setToolTip(i18n( "

Polar Alignment Helper tool requires the following:

1. German Equatorial Mount

2. FOV >" " 0.5 degrees

For small FOVs, use the Legacy Polar Alignment Tool.

")); FOVDisabledLabel->show(); } if (opsAstrometry->kcfg_AstrometryUseImageScale->isChecked()) { int unitType = opsAstrometry->kcfg_AstrometryImageScaleUnits->currentIndex(); // Degrees if (unitType == 0) { double fov_low = qMin(fov_x / 60, fov_y / 60); double fov_high = qMax(fov_x / 60, fov_y / 60); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcmins else if (unitType == 1) { double fov_low = qMin(fov_x, fov_y); double fov_high = qMax(fov_x, fov_y); opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_low); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_high); Options::setAstrometryImageScaleLow(fov_low); Options::setAstrometryImageScaleHigh(fov_high); } // Arcsec per pixel else { opsAstrometry->kcfg_AstrometryImageScaleLow->setValue(fov_pixscale * 0.9); opsAstrometry->kcfg_AstrometryImageScaleHigh->setValue(fov_pixscale * 1.1); // 10% boundary Options::setAstrometryImageScaleLow(fov_pixscale * 0.9); Options::setAstrometryImageScaleHigh(fov_pixscale * 1.1); } } } QStringList Align::generateOptions(const QVariantMap &optionsMap) { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcmiutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg QStringList solver_args; // Start with always-used arguments solver_args << "-O" << "--no-plots"; // Now go over boolean options // noverify if (optionsMap.contains("noverify")) solver_args << "--no-verify"; // noresort if (optionsMap.contains("resort")) solver_args << "--resort"; // fits2fits if (optionsMap.contains("nofits2fits")) solver_args << "--no-fits2fits"; // downsample if (optionsMap.contains("downsample")) solver_args << "--downsample" << QString::number(optionsMap.value("downsample", 2).toInt()); // image scale low if (optionsMap.contains("scaleL")) solver_args << "-L" << QString::number(optionsMap.value("scaleL").toDouble()); // image scale high if (optionsMap.contains("scaleH")) solver_args << "-H" << QString::number(optionsMap.value("scaleH").toDouble()); // image scale units if (optionsMap.contains("scaleUnits")) solver_args << "-u" << optionsMap.value("scaleUnits").toString(); // RA if (optionsMap.contains("ra")) solver_args << "-3" << QString::number(optionsMap.value("ra").toDouble()); // DE if (optionsMap.contains("de")) solver_args << "-4" << QString::number(optionsMap.value("de").toDouble()); // Radius if (optionsMap.contains("radius")) solver_args << "-5" << QString::number(optionsMap.value("radius").toDouble()); // Custom if (optionsMap.contains("custom")) solver_args << optionsMap.value("custom").toString(); return solver_args; } //This will generate the high and low scale of the imager field size based on the stated units. void Align::generateFOVBounds(double fov_h, double fov_v, QString &fov_low, QString &fov_high) { double fov_lower, fov_upper; // let's stretch the boundaries by 5% fov_lower = ((fov_h < fov_v) ? (fov_h * 0.95) : (fov_v * 0.95)); fov_upper = ((fov_h > fov_v) ? (fov_h * 1.05) : (fov_v * 1.05)); //No need to do anything if they are aw, since that is the default fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); } void Align::generateArgs() { // -O overwrite // -3 Expected RA // -4 Expected DEC // -5 Radius (deg) // -L lower scale of image in arcminutes // -H upper scale of image in arcmiutes // -u aw set scale to be in arcminutes // -W solution.wcs name of solution file // apog1.jpg name of target file to analyze //solve-field -O -3 06:40:51 -4 +09:49:53 -5 1 -L 40 -H 100 -u aw -W solution.wcs apod1.jpg QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) { if (Options::astrometryAutoDownsample() && ccd_width && ccd_height) { uint8_t bin = qMax(Options::solverBinningIndex() + 1, 1u); uint16_t w = ccd_width / bin; optionsMap["downsample"] = getSolverDownsample(w); } else optionsMap["downsample"] = Options::astrometryDownsample(); } if (Options::astrometryUseImageScale() && fov_x > 0 && fov_y > 0) { QString units = ImageScales[Options::astrometryImageScaleUnits()]; if (Options::astrometryAutoUpdateImageScale()) { QString fov_low, fov_high; double fov_w = fov_x; double fov_h = fov_y; if (units == "dw") { fov_w /= 60; fov_h /= 60; } else if (units == "app") { fov_w = fov_pixscale; fov_h = fov_pixscale; } generateFOVBounds(fov_w, fov_h, fov_low, fov_high); optionsMap["scaleL"] = fov_low; optionsMap["scaleH"] = fov_high; optionsMap["scaleUnits"] = units; } else { optionsMap["scaleL"] = Options::astrometryImageScaleLow(); optionsMap["scaleH"] = Options::astrometryImageScaleHigh(); optionsMap["scaleUnits"] = units; } } if (Options::astrometryUsePosition() && currentTelescope != nullptr) { double ra = 0, dec = 0; currentTelescope->getEqCoords(&ra, &dec); optionsMap["ra"] = ra * 15.0; optionsMap["de"] = dec; optionsMap["radius"] = Options::astrometryRadius(); } if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); QStringList solverArgs = generateOptions(optionsMap); QString options = solverArgs.join(" "); solverOptions->setText(options); solverOptions->setToolTip(options); } bool Align::captureAndSolve() { m_AlignTimer.stop(); m_CaptureTimer.stop(); #ifdef Q_OS_OSX if(solverTypeGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (currentCCD == nullptr) return false; if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: lost connection to CCD.")); KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed"), KSNotification::EVENT_ALERT); return false; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } // If CCD Telescope Type does not match desired scope type, change it // but remember current value so that it can be reset once capture is complete or is aborted. if (currentCCD->getTelescopeType() != FOVScopeCombo->currentIndex()) { rememberTelescopeType = currentCCD->getTelescopeType(); currentCCD->setTelescopeType(static_cast(FOVScopeCombo->currentIndex())); } if (parser->init() == false) return false; if (focal_length == -1 || aperture == -1) { KSNotification::error(i18n("Telescope aperture and focal length are missing. Please check your driver settings and try again.")); return false; } if (ccd_hor_pixel == -1 || ccd_ver_pixel == -1) { KSNotification::error(i18n("CCD pixel size is missing. Please check your driver settings and try again.")); return false; } if (currentFilter != nullptr) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: lost connection to filter wheel.")); return false; } int targetPosition = FilterPosCombo->currentIndex() + 1; if (targetPosition > 0 && targetPosition != currentFilterPosition) { filterPositionPending = true; filterManager->setFilterPosition(targetPosition); state = ALIGN_PROGRESS; return true; } } if (currentCCD->getDriverInfo()->getClientManager()->getBLOBMode(currentCCD->getDeviceName(), "CCD1") == B_NEVER) { if (KMessageBox::questionYesNo( nullptr, i18n("Image transfer is disabled for this camera. Would you like to enable it?")) == KMessageBox::Yes) { currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD1"); currentCCD->getDriverInfo()->getClientManager()->setBLOBMode(B_ONLY, currentCCD->getDeviceName(), "CCD2"); } else { return false; } } double seqExpose = exposureIN->value(); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (focusState >= FOCUS_PROGRESS) { appendLogText(i18n("Cannot capture while focus module is busy. Retrying in 10 seconds...")); m_CaptureTimer.start(); return false; } if (targetChip->isCapturing()) { appendLogText(i18n("Cannot capture while CCD exposure is in progress. Retrying in 10 seconds...")); m_CaptureTimer.start(); return false; } alignView->setBaseSize(alignWidget->size()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); connect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); // In case of remote solver, check if we need to update active CCD if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Update ACTIVE_CCD of the remote astrometry driver so it listens to BLOB emitted by the CCD ITextVectorProperty *activeDevices = remoteParserDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeCCD = IUFindText(activeDevices, "ACTIVE_CCD"); if (QString(activeCCD->text) != CCDCaptureCombo->currentText()) { IUSaveText(activeCCD, CCDCaptureCombo->currentText().toLatin1().data()); remoteParserDevice->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } // Enable remote parse dynamic_cast(remoteParser.get())->setEnabled(true); QString options = solverOptions->text().simplified(); QStringList solverArgs = options.split(' '); dynamic_cast(remoteParser.get())->sendArgs(solverArgs); // If mount model was reset, we do not update targetCoord // since the RA/DE is now different immediately after the reset // so we still try to lock for the coordinates before the reset. if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; solverTimer.start(); } //else //{ if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); // Remove temporary FITS files left before by the solver QDir dir(QDir::tempPath()); dir.setNameFilters(QStringList() << "fits*" << "tmp.*"); dir.setFilter(QDir::Files); for (auto &dirFile : dir.entryList()) dir.remove(dirFile); //} currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->resetFrame(); targetChip->setBatchMode(false); targetChip->setCaptureMode(FITS_ALIGN); targetChip->setFrameType(FRAME_LIGHT); int bin = Options::solverBinningIndex() + 1; targetChip->setBinning(bin, bin); // In case we're in refresh phase of the polar alignment helper then we use capture value from there if (pahStage == PAH_REFRESH) targetChip->capture(PAHExposure->value()); else targetChip->capture(seqExpose); Options::setAlignExposure(seqExpose); solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); differentialSlewingActivated = false; state = ALIGN_PROGRESS; emit newStatus(state); // If we're just refreshing, then we're done if (pahStage == PAH_REFRESH) return true; appendLogText(i18n("Capturing image...")); //This block of code will create the row in the solution table and populate RA, DE, and object name. //It also starts the progress indicator. double ra, dec; currentTelescope->getEqCoords(&ra, &dec); if (loadSlewState == IPS_IDLE) { int currentRow = solutionTable->rowCount(); solutionTable->insertRow(currentRow); for (int i = 4; i < 6; i++) { QTableWidgetItem *disabledBox = new QTableWidgetItem(); disabledBox->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, i, disabledBox); } QTableWidgetItem *RAReport = new QTableWidgetItem(); RAReport->setText(ScopeRAOut->text()); RAReport->setTextAlignment(Qt::AlignHCenter); RAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 0, RAReport); QTableWidgetItem *DECReport = new QTableWidgetItem(); DECReport->setText(ScopeDecOut->text()); DECReport->setTextAlignment(Qt::AlignHCenter); DECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 1, DECReport); double maxrad = 1.0; SkyObject *so = KStarsData::Instance()->skyComposite()->objectNearest(new SkyPoint(dms(ra * 15), dms(dec)), maxrad); QString name; if (so) { name = so->longname(); } else { name = "None"; } QTableWidgetItem *ObjNameReport = new QTableWidgetItem(); ObjNameReport->setText(name); ObjNameReport->setTextAlignment(Qt::AlignHCenter); ObjNameReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 2, ObjNameReport); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif QProgressIndicator *alignIndicator = new QProgressIndicator(this); solutionTable->setCellWidget(currentRow, 3, alignIndicator); alignIndicator->startAnimation(); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } return true; } void Align::newFITS(IBLOB *bp) { // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); blobType = *(static_cast(bp->aux1)); blobFileName = QString(static_cast(bp->aux2)); // If it's Refresh, we're done if (pahStage == PAH_REFRESH) { setCaptureComplete(); return; } appendLogText(i18n("Image received.")); if (solverTypeGroup->checkedId() != SOLVER_REMOTE) { if (blobType == ISD::CCD::BLOB_FITS) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (alignDarkFrameCheck->isChecked()) { int x, y, w, h, binx = 1, biny = 1; targetChip->getFrame(&x, &y, &w, &h); targetChip->getBinning(&binx, &biny); uint16_t offsetX = x / binx; uint16_t offsetY = y / biny; FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); alignDarkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Align::appendLogText); if (darkData) DarkLibrary::Instance()->subtract(darkData, alignView, FITS_NONE, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, alignView, exposureIN->value(), offsetX, offsetY); } return; } } setCaptureComplete(); } } void Align::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); if (pahStage == PAH_REFRESH) { newFrame(alignView); captureAndSolve(); return; } emit newImage(alignView); if (solverTypeGroup->checkedId() == SOLVER_ONLINE && Options::astrometryUseJPEG()) { ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); if (targetChip) { QString jpegFile = blobFileName + ".jpg"; bool rc = alignView->getDisplayImage().save(jpegFile, "JPG"); if (rc) blobFileName = jpegFile; } } if (getSolverFOV()) getSolverFOV()->setImage(alignView->getDisplayImage()); startSolving(blobFileName); } void Align::setSolverAction(int mode) { gotoModeButtonGroup->button(mode)->setChecked(true); currentGotoMode = static_cast(mode); } void Align::startSolving(const QString &filename, bool isGenerated) { QStringList solverArgs; QString options = solverOptions->text().simplified(); if (isGenerated) { solverArgs = options.split(' '); // Replace RA and DE with LST & 90/-90 pole if (pahStage == PAH_FIRST_CAPTURE) { for (int i = 0; i < solverArgs.count(); i++) { // RA if (solverArgs[i] == "-3") solverArgs[i + 1] = QString::number(KStarsData::Instance()->lst()->Degrees()); // DE. +90 for Northern hemisphere. -90 for southern hemisphere else if (solverArgs[i] == "-4") solverArgs[i + 1] = QString::number(hemisphere == NORTH_HEMISPHERE ? 90 : -90); } } } else if (filename.endsWith(QLatin1String("fits")) || filename.endsWith(QLatin1String("fit"))) { solverArgs = getSolverOptionsFromFITS(filename); appendLogText(i18n("Using solver options: %1", solverArgs.join(' '))); } else { KGuiItem blindItem(i18n("Blind solver"), QString(), i18n("Blind solver takes a very long time to solve but can reliably solve any image any " "where in the sky given enough time.")); KGuiItem existingItem(i18n("Use existing settings"), QString(), i18n("Mount must be pointing close to the target location and current field of view must " "match the image's field of view.")); int rc = KMessageBox::questionYesNoCancel(nullptr, i18n("No metadata is available in this image. Do you want to use the " "blind solver or the existing solver settings?"), i18n("Astrometry solver"), blindItem, existingItem, KStandardGuiItem::cancel(), "blind_solver_or_existing_solver_option"); if (rc == KMessageBox::Yes) { QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); solverArgs = generateOptions(optionsMap); } else if (rc == KMessageBox::No) solverArgs = options.split(' '); else { abort(); return; } } if (solverIterations == 0 && mountModelReset == false) { double ra, dec; currentTelescope->getEqCoords(&ra, &dec); targetCoord.setRA(ra); targetCoord.setDec(dec); } mountModelReset = false; //Options::setSolverOptions(solverOptions->text()); //Options::setGuideScopeCCDs(guideScopeCCDs); Options::setSolverAccuracyThreshold(accuracySpin->value()); Options::setAlignDarkFrame(alignDarkFrameCheck->isChecked()); Options::setSolverGotoOption(currentGotoMode); //m_isSolverComplete = false; //m_isSolverSuccessful = false; if (fov_x > 0) parser->verifyIndexFiles(fov_x, fov_y); solverTimer.start(); m_AlignTimer.start(); if (currentGotoMode == GOTO_SLEW) appendLogText(i18n("Solver iteration #%1", solverIterations + 1)); state = ALIGN_PROGRESS; emit newStatus(state); parser->startSovler(filename, solverArgs, isGenerated); } void Align::solverFinished(double orientation, double ra, double dec, double pixscale) { pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); sOrientation = orientation; sRA = ra; sDEC = dec; // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } m_AlignTimer.stop(); if (solverTypeGroup->checkedId() == SOLVER_REMOTE && remoteParser.get() != nullptr) { // Disable remote parse dynamic_cast(remoteParser.get())->setEnabled(false); } int binx, biny; ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); targetChip->getBinning(&binx, &biny); if (Options::alignmentLogging()) appendLogText(i18n("Solver RA (%1) DEC (%2) Orientation (%3) Pixel Scale (%4)", QString::number(ra, 'g', 5), QString::number(dec, 'g', 5), QString::number(orientation, 'g', 5), QString::number(pixscale, 'g', 5))); #if 0 if (pixscale > 0 && loadSlewState == IPS_IDLE) { double solver_focal_length = (206.264 * ccd_hor_pixel) / pixscale * binx; if (fabs(focal_length - solver_focal_length) > 1) appendLogText(i18n("Current focal length is %1 mm while computed focal length from the solver is %2 mm. " "Please update the mount focal length to obtain accurate results.", QString::number(focal_length, 'g', 5), QString::number(solver_focal_length, 'g', 5))); } #endif if (fov_x == 0 && pixscale > 0) { double newFOVW = ccd_width * pixscale / binx / 60.0; double newFOVH = ccd_height * pixscale / biny / 60.0; saveNewEffectiveFOV(newFOVW, newFOVH); } alignCoord.setRA0(ra / 15.0); alignCoord.setDec0(dec); RotOut->setText(QString::number(orientation, 'g', 5)); // Convert to JNow alignCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); // Get horizontal coords alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); // double raDiff = (alignCoord.ra().Degrees() - targetCoord.ra().Degrees()) * 3600; // double deDiff = (alignCoord.dec().Degrees() - targetCoord.dec().Degrees()) * 3600; double raDiff = (alignCoord.ra().deltaAngle(targetCoord.ra())).Degrees() * 3600; double deDiff = (alignCoord.dec().deltaAngle(targetCoord.dec())).Degrees() * 3600; dms RADiff(fabs(raDiff) / 3600.0), DEDiff(deDiff / 3600.0); dRAOut->setText(QString("%1%2").arg((raDiff > 0 ? "+" : "-"), RADiff.toHMSString())); dDEOut->setText(DEDiff.toDMSString(true)); pixScaleOut->setText(QString::number(pixscale, 'f', 2)); //emit newSolutionDeviation(raDiff, deDiff); targetDiff = sqrt(raDiff * raDiff + deDiff * deDiff); // Because astrometry reads image upside-down (bottom to top), the orientation is rotated 180 degrees when compared to PA // PA = Orientation + 180 double solverPA = orientation + 180; // Limit PA to -180 to +180 if (solverPA > 180) solverPA -= 360; if (solverPA < -180) solverPA += 360; solverFOV->setCenter(alignCoord); solverFOV->setPA(solverPA); solverFOV->setImageDisplay(Options::astrometrySolverOverlay()); sensorFOV->setPA(solverPA); QString ra_dms, dec_dms; getFormattedCoords(alignCoord.ra().Hours(), alignCoord.dec().Degrees(), ra_dms, dec_dms); SolverRAOut->setText(ra_dms); SolverDecOut->setText(dec_dms); //This block of code will write the result into the solution table and plot it on the graph. int currentRow = solutionTable->rowCount() - 1; if (loadSlewState == IPS_IDLE) { QTableWidgetItem *dRAReport = new QTableWidgetItem(); if (dRAReport) { dRAReport->setText(QString::number(raDiff, 'f', 3) + "\""); dRAReport->setTextAlignment(Qt::AlignHCenter); dRAReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 4, dRAReport); } QTableWidgetItem *dDECReport = new QTableWidgetItem(); if (dDECReport) { dDECReport->setText(QString::number(deDiff, 'f', 3) + "\""); dDECReport->setTextAlignment(Qt::AlignHCenter); dDECReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 5, dDECReport); } double raPlot = raDiff; double decPlot = deDiff; alignPlot->graph(0)->addData(raPlot, decPlot); QCPItemText *textLabel = new QCPItemText(alignPlot); textLabel->setPositionAlignment(Qt::AlignVCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(raPlot, decPlot); textLabel->setColor(Qt::red); textLabel->setPadding(QMargins(0, 0, 0, 0)); textLabel->setBrush(Qt::white); //textLabel->setBrush(Qt::NoBrush); textLabel->setPen(Qt::NoPen); textLabel->setText(' ' + QString::number(solutionTable->rowCount()) + ' '); textLabel->setFont(QFont(font().family(), 8)); if (!alignPlot->xAxis->range().contains(raDiff)) { alignPlot->graph(0)->rescaleKeyAxis(true); alignPlot->yAxis->setScaleRatio(alignPlot->xAxis, 1.0); } if (!alignPlot->yAxis->range().contains(deDiff)) { alignPlot->graph(0)->rescaleValueAxis(true); alignPlot->xAxis->setScaleRatio(alignPlot->yAxis, 1.0); } alignPlot->replot(); } if (Options::astrometrySolverWCS()) { INumberVectorProperty *ccdRotation = currentCCD->getBaseDevice()->getNumber("CCD_ROTATION"); if (ccdRotation) { INumber *rotation = IUFindNumber(ccdRotation, "CCD_ROTATION_VALUE"); if (rotation) { ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); rotation->value = orientation; clientManager->sendNewNumber(ccdRotation); if (m_wcsSynced == false) { appendLogText( i18n("WCS information updated. Images captured from this point forward shall have valid WCS.")); // Just send telescope info in case the CCD driver did not pick up before. INumberVectorProperty *telescopeInfo = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (telescopeInfo) clientManager->sendNewNumber(telescopeInfo); m_wcsSynced = true; } } } } m_CaptureErrorCounter = 0; m_SlewErrorCounter = 0; m_CaptureTimeoutCounter = 0; appendLogText(i18n("Solution coordinates: RA (%1) DEC (%2) Telescope Coordinates: RA (%3) DEC (%4)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString(), telescopeCoord.ra().toHMSString(), telescopeCoord.dec().toDMSString())); if (loadSlewState == IPS_IDLE && currentGotoMode == GOTO_SLEW) { dms diffDeg(targetDiff / 3600.0); appendLogText(i18n("Target is within %1 degrees of solution coordinates.", diffDeg.toDMSString())); } if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); //if (syncR->isChecked() || nothingR->isChecked() || targetDiff <= accuracySpin->value()) // CONTINUE HERE //This block of code along with some sections in the switch below will set the status report in the solution table for this item. std::unique_ptr statusReport(new QTableWidgetItem()); if (loadSlewState == IPS_IDLE) { solutionTable->setCellWidget(currentRow, 3, new QWidget()); statusReport->setFlags(Qt::ItemIsSelectable); } // Update Rotator offsets if (currentRotator != nullptr) { // When Load&Slew image is solved, we check if we need to rotate the rotator to match the position angle of the image if (loadSlewState == IPS_BUSY && Options::astrometryUseRotator()) { loadSlewTargetPA = solverPA; qCDebug(KSTARS_EKOS_ALIGN) << "loaSlewTargetPA:" << loadSlewTargetPA; } else { INumberVectorProperty *absAngle = currentRotator->getBaseDevice()->getNumber("ABS_ROTATOR_ANGLE"); if (absAngle) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = solverPA; double rawAngle = absAngle->np[0].value; double offset = solverPA - (rawAngle * Options::pAMultiplier()); qCDebug(KSTARS_EKOS_ALIGN) << "Raw Rotator Angle:" << rawAngle << "Rotator PA:" << currentRotatorPA << "Rotator Offset:" << offset; Options::setPAOffset(offset); } if (absAngle && std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 > Options::astrometryRotatorThreshold()) { double rawAngle = (loadSlewTargetPA - Options::pAOffset()) / Options::pAMultiplier(); if (rawAngle < 0) rawAngle += 360; else if (rawAngle > 360) rawAngle -= 360; absAngle->np[0].value = rawAngle; ClientManager *clientManager = currentRotator->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(absAngle); appendLogText(i18n("Setting position angle to %1 degrees E of N...", loadSlewTargetPA)); return; } } } emit newSolverResults(orientation, ra, dec, pixscale); QJsonObject solution = { {"ra", SolverRAOut->text()}, {"de", SolverDecOut->text()}, {"dRA", dRAOut->text()}, {"dDE", dDEOut->text()}, {"pix", pixscale}, {"rot", orientation}, {"fov", FOVOut->text()}, }; emit newSolution(solution.toVariantMap()); switch (currentGotoMode) { case GOTO_SYNC: executeGOTO(); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } return; case GOTO_SLEW: if (loadSlewState == IPS_BUSY || targetDiff > static_cast(accuracySpin->value())) { if (loadSlewState == IPS_IDLE && ++solverIterations == MAXIMUM_SOLVER_ITERATIONS) { appendLogText(i18n("Maximum number of iterations reached. Solver failed.")); if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } solverFailed(); if (mountModelRunning) finishAlignmentPoint(false); return; } targetAccuracyNotMet = true; if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignWarning.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } executeGOTO(); return; } if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } appendLogText(i18n("Target is within acceptable range. Astrometric solver is successful.")); if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; case GOTO_NOTHING: if (loadSlewState == IPS_IDLE) { statusReport->setIcon(QIcon(":/icons/AlignSuccess.svg")); solutionTable->setItem(currentRow, 3, statusReport.release()); } if (mountModelRunning) { finishAlignmentPoint(true); if (mountModelRunning) return; } break; } KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (pahStage != PAH_IDLE) processPAHStage(orientation, ra, dec, pixscale); else if (azStage > AZ_INIT || altStage > ALT_INIT) executePolarAlign(); else { solveB->setEnabled(true); loadSlewB->setEnabled(true); } } void Align::solverFailed() { KSNotification::event(QLatin1String("AlignFailed"), i18n("Astrometry alignment failed with errors"), KSNotification::EVENT_ALERT); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); m_AlignTimer.stop(); azStage = AZ_INIT; altStage = ALT_INIT; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; //emit solverComplete(false); state = ALIGN_FAILED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } void Align::abort() { parser->stopSolver(); pi->stopAnimation(); stopB->setEnabled(false); solveB->setEnabled(true); loadSlewB->setEnabled(true); // Reset Telescope Type to remembered value if (rememberTelescopeType != ISD::CCD::TELESCOPE_UNKNOWN) { currentCCD->setTelescopeType(rememberTelescopeType); rememberTelescopeType = ISD::CCD::TELESCOPE_UNKNOWN; } azStage = AZ_INIT; altStage = ALT_INIT; //loadSlewMode = false; loadSlewState = IPS_IDLE; solverIterations = 0; m_CaptureErrorCounter = 0; m_CaptureTimeoutCounter = 0; m_SlewErrorCounter = 0; m_AlignTimer.stop(); //currentCCD->disconnect(this); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Align::newFITS); disconnect(currentCCD, &ISD::CCD::newExposureValue, this, &Ekos::Align::checkCCDExposureProgress); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); ISD::CCDChip *targetChip = currentCCD->getChip(useGuideHead ? ISD::CCDChip::GUIDE_CCD : ISD::CCDChip::PRIMARY_CCD); // If capture is still in progress, let's stop that. if (pahStage == PAH_REFRESH) { if (targetChip->isCapturing()) targetChip->abortExposure(); appendLogText(i18n("Refresh is complete.")); } else { if (targetChip->isCapturing()) { targetChip->abortExposure(); appendLogText(i18n("Capture aborted.")); } else { int elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); appendLogText(i18np("Solver aborted after %1 second.", "Solver aborted after %1 seconds", elapsed)); } } state = ALIGN_ABORTED; emit newStatus(state); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); } QList Align::getSolutionResult() { QList result; result << sOrientation << sRA << sDEC; return result; } void Align::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_ALIGN) << text; emit newLog(text); } void Align::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Align::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "DOME_MOTION")) { // If dome is not ready and state is now if (domeReady == false && svp->s == IPS_OK) { domeReady = true; // trigger process number for mount so that it proceeds with normal workflow since // it was stopped by dome not being ready INumberVectorProperty *nvp = nullptr; if (currentTelescope->isJ2000()) nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_COORD"); else nvp = currentTelescope->getBaseDevice()->getNumber("EQUATORIAL_EOD_COORD"); if (nvp) processNumber(nvp); } } } void Align::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD")) { QString ra_dms, dec_dms; if (!strcmp(nvp->name, "EQUATORIAL_COORD")) { telescopeCoord.setRA0(nvp->np[0].value); telescopeCoord.setDec0(nvp->np[1].value); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(nvp->np[0].value); telescopeCoord.setDec(nvp->np[1].value); } getFormattedCoords(telescopeCoord.ra().Hours(), telescopeCoord.dec().Degrees(), ra_dms, dec_dms); telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); ScopeRAOut->setText(ra_dms); ScopeDecOut->setText(dec_dms); // qCDebug(KSTARS_EKOS_ALIGN) << "## RA" << ra_dms << "DE" << dec_dms // << "state:" << pstateStr(nvp->s) << "slewStarted?" << m_wasSlewStarted; switch (nvp->s) { // Idle --> Mount not tracking or slewing case IPS_IDLE: m_wasSlewStarted = false; //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_IDLE --> setting slewStarted to FALSE"; break; // Ok --> Mount Tracking. If m_wasSlewStarted is true // then it just finished slewing case IPS_OK: { // Update the boxes as the mount just finished slewing if (m_wasSlewStarted && Options::astrometryAutoUpdatePosition()) { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_OK --> Auto Update Position..."; opsAstrometry->estRA->setText(ra_dms); opsAstrometry->estDec->setText(dec_dms); Options::setAstrometryPositionRA(nvp->np[0].value * 15); Options::setAstrometryPositionDE(nvp->np[1].value); generateArgs(); } // If dome is syncing, wait until it stops if (currentDome && currentDome->isMoving()) { domeReady = false; return; } // If we are looking for celestial pole if (m_wasSlewStarted && pahStage == PAH_FIND_CP) { //qCDebug(KSTARS_EKOS_ALIGN) << "## PAH_FIND_CP--> setting slewStarted to FALSE"; m_wasSlewStarted = false; appendLogText(i18n("Mount completed slewing near celestial pole. Capture again to verify.")); setSolverAction(GOTO_NOTHING); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } // if (m_wasSlewStarted && pahStage == PAH_FIRST_ROTATE) // { // m_wasSlewStarted = false; // appendLogText(i18n("Mount first rotation is complete.")); // pahStage = PAH_SECOND_CAPTURE; // emit newPAHStage(pahStage); // PAHWidgets->setCurrentWidget(PAHSecondCapturePage); // emit newPAHMessage(secondCaptureText->text()); // if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) // appendLogText(i18n("Settling...")); // QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); // return; // } // else if (m_wasSlewStarted && pahStage == PAH_SECOND_ROTATE) // { // m_wasSlewStarted = false; // appendLogText(i18n("Mount second rotation is complete.")); // pahStage = PAH_THIRD_CAPTURE; // emit newPAHStage(pahStage); // PAHWidgets->setCurrentWidget(PAHThirdCapturePage); // emit newPAHMessage(thirdCaptureText->text()); // if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) // appendLogText(i18n("Settling...")); // QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); // return; // } switch (state) { case ALIGN_PROGRESS: break; case ALIGN_SYNCING: { m_wasSlewStarted = false; //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SYNCING --> setting slewStarted to FALSE"; if (currentGotoMode == GOTO_SLEW) { Slew(); return; } else { appendLogText(i18n("Mount is synced to solution coordinates. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } } break; case ALIGN_SLEWING: // If mount has not started slewing yet, then skip //qCDebug(KSTARS_EKOS_ALIGN) << "## Mount has not started slewing yet..."; if (m_wasSlewStarted == false) break; //qCDebug(KSTARS_EKOS_ALIGN) << "## ALIGN_SLEWING --> setting slewStarted to FALSE"; m_wasSlewStarted = false; if (loadSlewState == IPS_BUSY) { loadSlewState = IPS_IDLE; qCDebug(KSTARS_EKOS_ALIGN) << "loadSlewState is IDLE."; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); return; } else if (differentialSlewingActivated) { appendLogText(i18n("Differential slewing complete. Astrometric solver is successful.")); KSNotification::event(QLatin1String("AlignSuccessful"), i18n("Astrometry alignment completed successfully")); state = ALIGN_COMPLETE; emit newStatus(state); solverIterations = 0; if (mountModelRunning) finishAlignmentPoint(true); } else if (currentGotoMode == GOTO_SLEW || mountModelRunning) { if (targetAccuracyNotMet) appendLogText(i18n("Slew complete. Target accuracy is not met, running solver again...")); else appendLogText(i18n("Slew complete. Solving Alignment Point. . .")); targetAccuracyNotMet = false; state = ALIGN_PROGRESS; emit newStatus(state); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); return; } break; default: { //qCDebug(KSTARS_EKOS_ALIGN) << "## Align State " << state << "--> setting slewStarted to FALSE"; m_wasSlewStarted = false; } break; } } break; // Busy --> Mount Slewing or Moving (NSWE buttons) case IPS_BUSY: { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_BUSY --> setting slewStarted to TRUE"; m_wasSlewStarted = true; } break; // Alert --> Mount has problem moving or communicating. case IPS_ALERT: { //qCDebug(KSTARS_EKOS_ALIGN) << "## IPS_ALERT --> setting slewStarted to FALSE"; m_wasSlewStarted = false; if (state == ALIGN_SYNCING || state == ALIGN_SLEWING) { if (state == ALIGN_SYNCING) appendLogText(i18n("Syncing failed.")); else appendLogText(i18n("Slewing failed.")); if (++m_SlewErrorCounter == 3) { abort(); return; } else { if (currentGotoMode == GOTO_SLEW) Slew(); else Sync(); } } return; } } if (pahStage == PAH_FIRST_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "First mount rotation remainging degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount first rotation is complete.")); pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); emit newPAHMessage(secondCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } else if (pahStage == PAH_SECOND_ROTATE) { // only wait for telescope to slew to new position if manual slewing is switched off if(!PAHManual->isChecked()) { double deltaAngle = fabs(telescopeCoord.ra().deltaAngle(targetPAH.ra()).Degrees()); qCDebug(KSTARS_EKOS_ALIGN) << "Second mount rotation remainging degrees:" << deltaAngle; if (deltaAngle <= PAH_ROTATION_THRESHOLD) { currentTelescope->StopWE(); appendLogText(i18n("Mount second rotation is complete.")); pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); emit newPAHMessage(thirdCaptureText->text()); if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } // If for some reason we didn't stop, let's stop if we get too far else if (deltaAngle > PAHRotationSpin->value() * 1.25) { currentTelescope->Abort(); appendLogText(i18n("Mount aborted. Please restart the process and reduce the speed.")); stopPAHProcess(); } return; } // endif not manual slew } switch (azStage) { case AZ_SYNCING: if (currentTelescope->isSlewing()) azStage = AZ_SLEWING; break; case AZ_SLEWING: if (currentTelescope->isSlewing() == false) { azStage = AZ_SECOND_TARGET; measureAzError(); } break; case AZ_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust azimuth knob until the target is in the center of the view.")); azStage = AZ_INIT; } break; default: break; } switch (altStage) { case ALT_SYNCING: if (currentTelescope->isSlewing()) altStage = ALT_SLEWING; break; case ALT_SLEWING: if (currentTelescope->isSlewing() == false) { altStage = ALT_SECOND_TARGET; measureAltError(); } break; case ALT_CORRECTING: if (currentTelescope->isSlewing() == false) { appendLogText(i18n( "Slew complete. Please adjust altitude knob until the target is in the center of the view.")); altStage = ALT_INIT; } break; default: break; } } else if (!strcmp(nvp->name, "ABS_ROTATOR_ANGLE")) { // PA = RawAngle * Multiplier + Offset currentRotatorPA = (nvp->np[0].value * Options::pAMultiplier()) + Options::pAOffset(); if (currentRotatorPA > 180) currentRotatorPA -= 360; if (currentRotatorPA < -180) currentRotatorPA += 360; if (std::isnan(loadSlewTargetPA) == false && fabs(currentRotatorPA - loadSlewTargetPA) * 60 <= Options::astrometryRotatorThreshold()) { appendLogText(i18n("Rotator reached target position angle.")); targetAccuracyNotMet = true; loadSlewTargetPA = std::numeric_limits::quiet_NaN(); QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::executeGOTO); } } // N.B. Ekos::Manager already mananges TELESCOPE_INFO, why here again? //if (!strcmp(coord->name, "TELESCOPE_INFO")) //syncTelescopeInfo(); } void Align::executeGOTO() { if (loadSlewState == IPS_BUSY) { //if (loadSlewIterations == loadSlewIterationsSpin->value()) //loadSlewCoord = alignCoord; //targetCoord = loadSlewCoord; targetCoord = alignCoord; SlewToTarget(); } else if (currentGotoMode == GOTO_SYNC) Sync(); else if (currentGotoMode == GOTO_SLEW) SlewToTarget(); } void Align::Sync() { state = ALIGN_SYNCING; if (currentTelescope->Sync(&alignCoord)) { emit newStatus(state); appendLogText( i18n("Syncing to RA (%1) DEC (%2)", alignCoord.ra().toHMSString(), alignCoord.dec().toDMSString())); } else { state = ALIGN_IDLE; emit newStatus(state); appendLogText(i18n("Syncing failed.")); } } void Align::Slew() { state = ALIGN_SLEWING; emit newStatus(state); //qCDebug(KSTARS_EKOS_ALIGN) << "## Before SLEW command: wasSlewStarted -->" << m_wasSlewStarted; //m_wasSlewStarted = currentTelescope->Slew(&targetCoord); //qCDebug(KSTARS_EKOS_ALIGN) << "## After SLEW command: wasSlewStarted -->" << m_wasSlewStarted; // JM 2019-08-23: Do not assume that slew was started immediately. Wait until IPS_BUSY state is triggered // from Goto currentTelescope->Slew(&targetCoord); appendLogText(i18n("Slewing to target coordinates: RA (%1) DEC (%2).", targetCoord.ra().toHMSString(), targetCoord.dec().toDMSString())); } void Align::SlewToTarget() { if (canSync && loadSlewState == IPS_IDLE) { // 2018-01-24 JM: This is ugly. Maybe use DBus? Signal/Slots? Ekos Manager usage like this should be avoided if (KStars::Instance()->ekosManager() && !KStars::Instance()->ekosManager()->getCurrentJobName().isEmpty()) { KSNotification::event(QLatin1String("EkosSchedulerTelescopeSynced"), i18n("Ekos job (%1) - Telescope synced", KStars::Instance()->ekosManager()->getCurrentJobName())); } // Do we perform a regular sync or use differential slewing? if (Options::astrometryDifferentialSlewing()) { dms raDiff = alignCoord.ra().deltaAngle(targetCoord.ra()); dms deDiff = alignCoord.dec().deltaAngle(targetCoord.dec()); targetCoord.setRA(targetCoord.ra() - raDiff); targetCoord.setDec(targetCoord.dec() - deDiff); differentialSlewingActivated = true; qCDebug(KSTARS_EKOS_ALIGN) << "Using differential slewing..."; Slew(); } else Sync(); return; } Slew(); } void Align::executePolarAlign() { appendLogText(i18n("Processing solution for polar alignment...")); switch (azStage) { case AZ_FIRST_TARGET: case AZ_FINISHED: measureAzError(); break; default: break; } switch (altStage) { case ALT_FIRST_TARGET: case ALT_FINISHED: measureAltError(); break; default: break; } } void Align::measureAzError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measureing Azimuth Error..."; switch (azStage) { case AZ_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel( nullptr, hemisphere == NORTH_HEMISPHERE ? i18n("Point the telescope at the southern meridian. Press Continue when ready.") : i18n("Point the telescope at the northern meridian. Press Continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_az_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame near the meridian.")); azStage = AZ_FIRST_TARGET; captureAndSolve(); break; case AZ_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { azStage = AZ_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { azStage = AZ_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case AZ_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame near the meridian.")); azStage = AZ_FINISHED; captureAndSolve(); break; case AZ_FINISHED: // Measure deviation in DEC // Call function to report error // set stage to AZ_FIRST_TARGET again appendLogText(i18n("Calculating azimuth alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); azStage = AZ_INIT; break; default: break; } } void Align::measureAltError() { static double initRA = 0, initDEC = 0, finalRA = 0, finalDEC = 0, initAz = 0; if (pahStage != PAH_IDLE && (KMessageBox::warningContinueCancel(KStars::Instance(), i18n("Polar Alignment Helper is still active. Do you want to continue " "using legacy polar alignment tool?")) != KMessageBox::Continue)) return; pahStage = PAH_IDLE; emit newPAHStage(pahStage); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Measureing Altitude Error..."; switch (altStage) { case ALT_INIT: // Display message box confirming user point scope near meridian and south // N.B. This action cannot be automated. if (KMessageBox::warningContinueCancel(nullptr, i18n("Point the telescope to the eastern or western horizon with a " "minimum altitude of 20 degrees. Press continue when ready."), i18n("Polar Alignment Measurement"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "ekos_measure_alt_error") != KMessageBox::Continue) return; appendLogText(i18n("Solving first frame.")); altStage = ALT_FIRST_TARGET; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); break; case ALT_FIRST_TARGET: // start solving there, find RA/DEC initRA = alignCoord.ra().Degrees(); initDEC = alignCoord.dec().Degrees(); initAz = alignCoord.az().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << alignCoord.ra().toHMSString() << " initDEC " << alignCoord.dec().toDMSString() << " initlAz " << alignCoord.az().toDMSString() << " initAlt " << alignCoord.alt().toDMSString(); // Now move 30 arcminutes in RA if (canSync) { altStage = ALT_SYNCING; currentTelescope->Sync(initRA / 15.0, initDEC); currentTelescope->Slew((initRA - RAMotion) / 15.0, initDEC); } // If telescope doesn't sync, we slew relative to its current coordinates else { altStage = ALT_SLEWING; currentTelescope->Slew(telescopeCoord.ra().Hours() - RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing 30 arcminutes in RA...")); break; case ALT_SECOND_TARGET: // We reached second target now // Let now solver for RA/DEC appendLogText(i18n("Solving second frame.")); altStage = ALT_FINISHED; if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); break; case ALT_FINISHED: // Measure deviation in DEC // Call function to report error appendLogText(i18n("Calculating altitude alignment error...")); finalRA = alignCoord.ra().Degrees(); finalDEC = alignCoord.dec().Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar finalRA " << alignCoord.ra().toHMSString() << " finalDEC " << alignCoord.dec().toDMSString() << " finalAz " << alignCoord.az().toDMSString() << " finalAlt " << alignCoord.alt().toDMSString(); // Slew back to original position if (canSync) currentTelescope->Slew(initRA / 15.0, initDEC); // If telescope doesn't sync, we slew relative to its current coordinates else { currentTelescope->Slew(telescopeCoord.ra().Hours() + RAMotion / 15.0, telescopeCoord.dec().Degrees()); } appendLogText(i18n("Slewing back to original position...")); calculatePolarError(initRA, initDEC, finalRA, finalDEC, initAz); altStage = ALT_INIT; break; default: break; } } void Align::calculatePolarError(double initRA, double initDEC, double finalRA, double finalDEC, double initAz) { double raMotion = finalRA - initRA; decDeviation = finalDEC - initDEC; // East/West of meridian int horizon = (initAz > 0 && initAz <= 180) ? 0 : 1; // How much time passed siderrally form initRA to finalRA? //double RATime = fabs(raMotion / SIDRATE) / 60.0; // 2016-03-30: Diff in RA is sufficient for time difference // raMotion in degrees. RATime in minutes. double RATime = fabs(raMotion) * 60.0; // Equation by Frank Berret (Measuring Polar Axis Alignment Error, page 4) // In degrees double deviation = (3.81 * (decDeviation * 3600)) / (RATime * cos(initDEC * dms::DegToRad)) / 60.0; dms devDMS(fabs(deviation)); KLocalizedString deviationDirection; switch (hemisphere) { // Northern hemisphere case NORTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far east"); else deviationDirection = ki18n("%1 too far west"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; default: break; } } break; // Southern hemisphere case SOUTH_HEMISPHERE: if (azStage == AZ_FINISHED) { if (decDeviation > 0) deviationDirection = ki18n("%1 too far west"); else deviationDirection = ki18n("%1 too far east"); } else if (altStage == ALT_FINISHED) { switch (horizon) { // East case 0: if (decDeviation > 0) deviationDirection = ki18n("%1 too far low"); else deviationDirection = ki18n("%1 too far high"); break; // West case 1: if (decDeviation > 0) deviationDirection = ki18n("%1 too far high"); else deviationDirection = ki18n("%1 too far low"); break; default: break; } } break; } qCDebug(KSTARS_EKOS_ALIGN) << "Polar Hemisphere is " << ((hemisphere == NORTH_HEMISPHERE) ? "North" : "South") << " --- initAz " << initAz; qCDebug(KSTARS_EKOS_ALIGN) << "Polar initRA " << initRA << " initDEC " << initDEC << " finalRA " << finalRA << " finalDEC " << finalDEC; qCDebug(KSTARS_EKOS_ALIGN) << "Polar decDeviation " << decDeviation * 3600 << " arcsec " << " RATime " << RATime << " minutes"; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Raw Deviation " << deviation << " degrees."; if (azStage == AZ_FINISHED) { azError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); //azError->setText(deviationDirection.subs(QString("%1")azDMS.toDMSString()); azDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Azimuth Deviation " << azDeviation << " degrees."; correctAzB->setEnabled(true); } if (altStage == ALT_FINISHED) { //altError->setText(deviationDirection.subs(QString("%1").arg(fabs(deviation), 0, 'g', 3)).toString()); altError->setText(deviationDirection.subs(QString("%1").arg(devDMS.toDMSString())).toString()); altDeviation = deviation * (decDeviation > 0 ? 1 : -1); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Altitude Deviation " << altDeviation << " degrees."; correctAltB->setEnabled(true); } } void Align::correctAltError() { double newRA, newDEC; SkyPoint currentCoord(telescopeCoord); dms targetLat; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Altitude Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); // An error in polar alignment altitude reflects a deviation in the latitude of the mount from actual latitude of the site // Calculating the latitude accounting for the altitude deviation. This is the latitude at which the altitude deviation should be zero. targetLat.setD(KStars::Instance()->data()->geo()->lat()->Degrees() + altDeviation); // Calculate the Az/Alt of the mount if it were located at the corrected latitude currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), &targetLat); // Convert corrected Az/Alt to RA/DEC given the local sideral time and current (not corrected) latitude currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // New RA/DEC should reflect the position in the sky at which the polar alignment altitude error is minimal. newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); altStage = ALT_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Latitude = Latitude " << KStars::Instance()->data()->geo()->lat()->Degrees() << " + Altitude Deviation " << altDeviation << " = " << targetLat.Degrees(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::correctAzError() { double newRA, newDEC, currentAlt, currentAz; SkyPoint currentCoord(telescopeCoord); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Correcting Azimuth Error..."; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Current Mount RA " << currentCoord.ra().toHMSString() << " DEC " << currentCoord.dec().toDMSString() << "Az " << currentCoord.az().toDMSString() << " Alt " << currentCoord.alt().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "Polar Target Azimuth = Current Azimuth " << currentCoord.az().Degrees() << " + Azimuth Deviation " << azDeviation << " = " << currentCoord.az().Degrees() + azDeviation; // Get current horizontal coordinates of the mount currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Keep Altitude as it is and change Azimuth to account for the azimuth deviation // The new sky position should be where the polar alignment azimuth error is minimal currentAlt = currentCoord.alt().Degrees(); currentAz = currentCoord.az().Degrees() + azDeviation; // Update current Alt and Azimuth to new values currentCoord.setAlt(currentAlt); currentCoord.setAz(currentAz); // Convert Alt/Az back to equatorial coordinates currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); // Get new RA and DEC newRA = currentCoord.ra().Hours(); newDEC = currentCoord.dec().Degrees(); azStage = AZ_CORRECTING; qCDebug(KSTARS_EKOS_ALIGN) << "Polar Slewing to calibration position..."; currentTelescope->Slew(newRA, newDEC); appendLogText(i18n("Slewing to calibration position, please wait until telescope completes slewing.")); } void Align::getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str) { dms ra_s, dec_s; ra_s.setH(ra); dec_s.setD(dec); ra_str = QString("%1:%2:%3") .arg(ra_s.hour(), 2, 10, QChar('0')) .arg(ra_s.minute(), 2, 10, QChar('0')) .arg(ra_s.second(), 2, 10, QChar('0')); if (dec_s.Degrees() < 0) dec_str = QString("-%1:%2:%3") .arg(abs(dec_s.degree()), 2, 10, QChar('0')) .arg(abs(dec_s.arcmin()), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); else dec_str = QString("%1:%2:%3") .arg(dec_s.degree(), 2, 10, QChar('0')) .arg(dec_s.arcmin(), 2, 10, QChar('0')) .arg(dec_s.arcsec(), 2, 10, QChar('0')); } bool Align::loadAndSlew(QString fileURL) { /*if (solverTypeGroup->checkedId() == SOLVER_REMOTE) { appendLogText(i18n("Load and Slew is not supported in remote solver mode.")); loadSlewB->setEnabled(false); return; }*/ #ifdef Q_OS_OSX if(solverTypeGroup->checkedId() == SOLVER_OFFLINE) { if(Options::useDefaultPython()) { if( !opsAlign->astropyInstalled() || !opsAlign->pythonInstalled() ) { KSNotification::error(i18n("Astrometry.net uses python3 and the astropy package for plate solving images offline. These were not detected on your system. Please go into the Align Options and either click the setup button to install them or uncheck the default button and enter the path to python3 on your system and manually install astropy.")); return false; } } } #endif if (fileURL.isEmpty()) fileURL = QFileDialog::getOpenFileName(KStars::Instance(), i18n("Load Image"), dirPath, "Images (*.fits *.fit *.jpg *.jpeg)"); if (fileURL.isEmpty()) return false; QFileInfo fileInfo(fileURL); dirPath = fileInfo.absolutePath(); differentialSlewingActivated = false; loadSlewState = IPS_BUSY; stopPAHProcess(); slewR->setChecked(true); currentGotoMode = GOTO_SLEW; solveB->setEnabled(false); stopB->setEnabled(true); pi->startAnimation(); startSolving(fileURL, false); return true; } void Align::setExposure(double value) { exposureIN->setValue(value); } void Align::setBinningIndex(int binIndex) { syncSettings(); Options::setSolverBinningIndex(binIndex); // If sender is not our combo box, then we need to update the combobox itself if (dynamic_cast(sender()) != binningCombo) { binningCombo->blockSignals(true); binningCombo->setCurrentIndex(binIndex); binningCombo->blockSignals(false); } // Need to calculate FOV and args for APP if (Options::astrometryImageScaleUnits() == OpsAstrometry::SCALE_ARCSECPERPIX) { calculateFOV(); generateArgs(); } } void Align::setSolverArguments(const QString &value) { solverOptions->setText(value); } QString Align::solverArguments() { return solverOptions->text(); } void Align::setFOVTelescopeType(int index) { FOVScopeCombo->setCurrentIndex(index); } FOV *Align::getSolverFOV() { if (sOrientation == -1) return nullptr; else return solverFOV.get(); } void Align::addFilter(ISD::GDInterface *newFilter) { for (auto filter : Filters) { if (!strcmp(filter->getDeviceName(), newFilter->getDeviceName())) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); checkFilter(1); FilterDevicesCombo->setCurrentIndex(1); } bool Align::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Align::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Align::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Align::filter() { return FilterPosCombo->currentText(); } void Align::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(Options::lockAlignFilterIndex()); syncSettings(); } void Align::setWCSEnabled(bool enable) { if (currentCCD == nullptr) return; ISwitchVectorProperty *wcsControl = currentCCD->getBaseDevice()->getSwitch("WCS_CONTROL"); ISwitch *wcs_enable = IUFindSwitch(wcsControl, "WCS_ENABLE"); ISwitch *wcs_disable = IUFindSwitch(wcsControl, "WCS_DISABLE"); if (!wcs_enable || !wcs_disable) return; if ((wcs_enable->s == ISS_ON && enable) || (wcs_disable->s == ISS_ON && !enable)) return; IUResetSwitch(wcsControl); if (enable) { appendLogText(i18n("World Coordinate System (WCS) is enabled. CCD rotation must be set either manually in the " "CCD driver or by solving an image before proceeding to capture any further images, " "otherwise the WCS information may be invalid.")); wcs_enable->s = ISS_ON; } else { wcs_disable->s = ISS_ON; m_wcsSynced = false; appendLogText(i18n("World Coordinate System (WCS) is disabled.")); } ClientManager *clientManager = currentCCD->getDriverInfo()->getClientManager(); clientManager->sendNewSwitch(wcsControl); } void Align::checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state) { INDI_UNUSED(targetChip); INDI_UNUSED(remaining); if (state == IPS_ALERT) { if (++m_CaptureErrorCounter == 3 && pahStage != PAH_REFRESH) { appendLogText(i18n("Capture error. Aborting...")); abort(); return; } appendLogText(i18n("Restarting capture attempt #%1", m_CaptureErrorCounter)); int currentRow = solutionTable->rowCount() - 1; solutionTable->setCellWidget(currentRow, 3, new QWidget()); QTableWidgetItem *statusReport = new QTableWidgetItem(); statusReport->setIcon(QIcon(":/icons/AlignFailure.svg")); statusReport->setFlags(Qt::ItemIsSelectable); solutionTable->setItem(currentRow, 3, statusReport); captureAndSolve(); } } void Align::setFocusStatus(Ekos::FocusState state) { focusState = state; } QStringList Align::getSolverOptionsFromFITS(const QString &filename) { int status = 0, fits_ccd_width, fits_ccd_height, fits_binx = 1, fits_biny = 1; char comment[128], error_status[512]; fitsfile *fptr = nullptr; double ra = 0, dec = 0, fits_fov_x, fits_fov_y, fov_lower, fov_upper, fits_ccd_hor_pixel = -1, fits_ccd_ver_pixel = -1, fits_focal_length = -1; QString fov_low, fov_high; QStringList solver_args; QVariantMap optionsMap; if (Options::astrometryUseNoVerify()) optionsMap["noverify"] = true; if (Options::astrometryUseResort()) optionsMap["resort"] = true; if (Options::astrometryUseNoFITS2FITS()) optionsMap["nofits2fits"] = true; if (Options::astrometryUseDownsample()) optionsMap["downsample"] = Options::astrometryDownsample(); if (Options::astrometryCustomOptions().isEmpty() == false) optionsMap["custom"] = Options::astrometryCustomOptions(); solver_args = generateOptions(optionsMap); status = 0; #if 0 if (fits_open_image(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCritical(KSTARS_EKOS_ALIGN) << "Could not open file " << filename << " Error: " << QString::fromUtf8(error_status); return solver_args; } #endif // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_ALIGN) << QString::fromUtf8(error_status); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS1", &fits_ccd_width, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS1.")); return solver_args; } status = 0; if (fits_read_key(fptr, TINT, "NAXIS2", &fits_ccd_height, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find NAXIS2.")); return solver_args; } // If we need to auto downsample, let us figure out the scale and regenerate options if (Options::astrometryAutoDownsample()) { optionsMap["downsample"] = getSolverDownsample(fits_ccd_width); solver_args = generateOptions(optionsMap); } bool coord_ok = true; status = 0; char objectra_str[32]; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); } else // Degrees to hours ra /= 15; } else { dms raDMS = dms::fromString(objectra_str, false); ra = raDMS.Hours(); } status = 0; char objectde_str[32]; if (coord_ok && fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); coord_ok = false; appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); } } else { dms deDMS = dms::fromString(objectde_str, true); dec = deDMS.Degrees(); } /*if (coord_ok == false) { ra = telescopeCoord.ra0().Hours(); dec = telescopeCoord.dec0().Degrees(); }*/ if (coord_ok && Options::astrometryUsePosition()) solver_args << "-3" << QString::number(ra * 15.0) << "-4" << QString::number(dec) << "-5" << "15"; status = 0; double pixelScale = 0; // If we have pixel scale in arcsecs per pixel then lets use that directly // instead of calculating it from FOCAL length and other information if (fits_read_key(fptr, TDOUBLE, "SCALE", &pixelScale, comment, &status) == 0) { fov_low = QString::number(0.9 * pixelScale); fov_high = QString::number(1.1 * pixelScale); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "app"; return solver_args; } if (fits_read_key(fptr, TDOUBLE, "FOCALLEN", &fits_focal_length, comment, &status)) { int integer_focal_length = -1; if (fits_read_key(fptr, TINT, "FOCALLEN", &integer_focal_length, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find FOCALLEN (%1).", QString(error_status))); return solver_args; } else fits_focal_length = integer_focal_length; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE1", &fits_ccd_hor_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE1 (%1).", QString(error_status))); return solver_args; } status = 0; if (fits_read_key(fptr, TDOUBLE, "PIXSIZE2", &fits_ccd_ver_pixel, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find PIXSIZE2 (%1).", QString(error_status))); return solver_args; } status = 0; fits_read_key(fptr, TINT, "XBINNING", &fits_binx, comment, &status); status = 0; fits_read_key(fptr, TINT, "YBINNING", &fits_biny, comment, &status); // Calculate FOV fits_fov_x = 206264.8062470963552 * fits_ccd_width * fits_ccd_hor_pixel / 1000.0 / fits_focal_length * fits_binx; fits_fov_y = 206264.8062470963552 * fits_ccd_height * fits_ccd_ver_pixel / 1000.0 / fits_focal_length * fits_biny; fits_fov_x /= 60.0; fits_fov_y /= 60.0; // let's stretch the boundaries by 10% fov_lower = qMin(fits_fov_x, fits_fov_y); fov_upper = qMax(fits_fov_x, fits_fov_y); fov_lower *= 0.90; fov_upper *= 1.10; fov_low = QString::number(fov_lower); fov_high = QString::number(fov_upper); if (Options::astrometryUseImageScale()) solver_args << "-L" << fov_low << "-H" << fov_high << "-u" << "aw"; return solver_args; } uint8_t Align::getSolverDownsample(uint16_t binnedW) { uint8_t downsample = Options::astrometryDownsample(); if (!Options::astrometryAutoDownsample()) return downsample; while (downsample < 8) { if (binnedW / downsample <= 1024) break; downsample += 2; } return downsample; } void Align::saveSettleTime() { Options::setSettlingTime(delaySpin->value()); } void Align::setCaptureStatus(CaptureState newState) { switch (newState) { case CAPTURE_ALIGNING: if (currentTelescope && currentTelescope->hasAlignmentModel() && Options::resetMountModelAfterMeridian()) { mountModelReset = currentTelescope->clearAlignmentModel(); qCDebug(KSTARS_EKOS_ALIGN) << "Post meridian flip mount model reset" << (mountModelReset ? "successful." : "failed."); } QTimer::singleShot(Options::settlingTime(), this, &Ekos::Align::captureAndSolve); break; default: break; } } void Align::showFITSViewer() { FITSData *data = alignView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Align::toggleAlignWidgetFullScreen() { if (alignWidget->parent() == nullptr) { alignWidget->setParent(this); rightLayout->insertWidget(0, alignWidget); //rightLayout->setStretch(0, 2); // rightLayout->setStretch(1, 1); alignWidget->showNormal(); } else { alignWidget->setParent(nullptr); alignWidget->setWindowTitle(i18n("Align Frame")); alignWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); alignWidget->showMaximized(); alignWidget->show(); } } void Align::startPAHProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant process..."; pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); nothingR->setChecked(true); currentGotoMode = GOTO_NOTHING; loadSlewB->setEnabled(false); rememberSolverWCS = Options::astrometrySolverWCS(); rememberAutoWCS = Options::autoWCS(); Options::setAutoWCS(false); Options::setAstrometrySolverWCS(true); if (Options::limitedResourcesMode()) appendLogText(i18n("Warning: Equatorial Grid Lines will not be drawn due to limited resources mode.")); if (currentTelescope->hasAlignmentModel()) { appendLogText(i18n("Clearing mount Alignment Model...")); mountModelReset = currentTelescope->clearAlignmentModel(); } // Unpark currentTelescope->UnPark(); // Set tracking ON if not already if (currentTelescope->canControlTrack() && currentTelescope->isTracking() == false) currentTelescope->setTrackEnabled(true); PAHStartB->setEnabled(false); PAHStopB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHFirstCapturePage); emit newPAHMessage(firstCaptureText->text()); captureAndSolve(); } void Align::stopPAHProcess() { if (pahStage == PAH_IDLE) return; qCInfo(KSTARS_EKOS_ALIGN) << "Stopping Polar Alignment Assistant process..."; // Only display dialog if user explicitly restarts if ((static_cast(sender()) == PAHStopB) && KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to stop the polar alignment process?"), i18n("Polar Alignment Assistant"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "restart_PAA_process_dialog") == KMessageBox::No) return; stopB->click(); if (currentTelescope && currentTelescope->isInMotion()) currentTelescope->Abort(); pahStage = PAH_IDLE; emit newPAHStage(pahStage); PAHStartB->setEnabled(true); PAHStopB->setEnabled(false); PAHRefreshB->setEnabled(true); PAHWidgets->setCurrentWidget(PAHIntroPage); emit newPAHMessage(introText->text()); qDeleteAll(pahImageInfos); pahImageInfos.clear(); correctionVector = QLineF(); correctionOffset = QPointF(); alignView->setCorrectionParams(correctionVector); alignView->setCorrectionOffset(correctionOffset); alignView->setRACircle(QVector3D()); alignView->setRefreshEnabled(false); emit newFrame(alignView); disconnect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); disconnect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector); if (Options::pAHAutoPark()) { currentTelescope->Park(); appendLogText(i18n("Parking the mount...")); } state = ALIGN_IDLE; emit newStatus(state); } void Align::rotatePAH() { double raDiff = PAHRotationSpin->value(); bool westMeridian = PAHDirectionCombo->currentIndex() == 0; // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; // JM 2018-05-03: Hemispheres shouldn't affect rotation direction in RA #if 0 // North if (hemisphere == NORTH_HEMISPHERE) { // West if (westMeridian) raDiff *= -1; // East else raDiff *= 1; } // South else { // West if (westMeridian) raDiff *= 1; // East else raDiff *= -1; } #endif // if Manual slewing is selected, don't move the mount if (PAHManual->isChecked()) { appendLogText(i18n("Please rotate your mount about %1deg in RA", raDiff )); return; } // raDiff is in degrees dms newTelescopeRA = (telescopeCoord.ra() + dms(raDiff)).reduce(); targetPAH.setRA(newTelescopeRA); targetPAH.setDec(telescopeCoord.dec()); //currentTelescope->Slew(&targetPAH); // Set Selected Speed currentTelescope->setSlewRate(PAHSlewRateCombo->currentIndex()); // Go to direction currentTelescope->MoveWE(westMeridian ? ISD::Telescope::MOTION_WEST : ISD::Telescope::MOTION_EAST, ISD::Telescope::MOTION_START); appendLogText(i18n("Please wait until mount completes rotating to RA (%1) DE (%2)", targetPAH.ra().toHMSString(), targetPAH.dec().toDMSString())); } void Align::calculatePAHError() { QVector3D RACircle; bool rc = findRACircle(RACircle); if (rc == false) { appendLogText(i18n("Failed to find a solution. Try again.")); stopPAHProcess(); return; } if (alignView->isEQGridShown() == false) alignView->toggleEQGrid(); alignView->setRACircle(RACircle); FITSData *imageData = alignView->getImageData(); QPointF RACenterPoint(RACircle.x(), RACircle.y()); SkyPoint RACenter; rc = imageData->pixelToWCS(RACenterPoint, RACenter); if (rc == false) { appendLogText(i18n("Failed to find RA Axis center: %1.", imageData->getLastError())); return; } SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); RACenter.setRA(RACenter.ra0()); RACenter.setDec(RACenter.dec0()); double PA = 0; dms polarError = RACenter.angularDistanceTo(&CP, &PA); if (Options::alignmentLogging()) { qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Circle X: " << RACircle.x() << " Y: " << RACircle.y() << " Radius: " << RACircle.z(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Location RA: " << RACenter.ra0().toHMSString() << "DE: " << RACenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "RA Axis Offset: " << polarError.toDMSString() << "PA:" << PA; qCDebug(KSTARS_EKOS_ALIGN) << "CP Axis Location X:" << celestialPolePoint.x() << "Y:" << celestialPolePoint.y(); } RACenter.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); QString azDirection = RACenter.az().Degrees() < 30 ? "Right" : "Left"; QString atDirection = RACenter.alt().Degrees() < KStarsData::Instance()->geo()->lat()->Degrees() ? "Bottom" : "Top"; // FIXME should this be reversed for southern hemisphere? appendLogText(i18n("Mount axis is to the %1 %2 of the celestial pole", atDirection, azDirection)); PAHErrorLabel->setText(polarError.toDMSString()); // JM 2019-08-17: Flip for southern hemisphere. // Possible fix for: https://indilib.org/forum/ekos/5558-ekos-polar-alignment-vector-backwards.html correctionVector.setP1((hemisphere == NORTH_HEMISPHERE) ? celestialPolePoint : RACenterPoint); correctionVector.setP2((hemisphere == NORTH_HEMISPHERE) ? RACenterPoint : celestialPolePoint); /* bool RAAxisInside = imageData->contains(RACenterPoint); bool CPPointInside= imageData->contains(celestialPolePoint); if (RAAxisInside == false && CPPointInside == false) appendLogText(i18n("Warning: Mount axis and celestial pole are outside the field of view. Correction vector may be inaccurate.")); */ connect(alignView, &AlignView::trackingStarSelected, this, &Ekos::Align::setPAHCorrectionOffset); emit polarResultUpdated(correctionVector, polarError.toDMSString()); connect(alignView, &AlignView::newCorrectionVector, this, &Ekos::Align::newCorrectionVector, Qt::UniqueConnection); emit newCorrectionVector(correctionVector); alignView->setCorrectionParams(correctionVector); emit newFrame(alignView); } void Align::setPAHCorrectionOffsetPercentage(double dx, double dy) { double x = dx * alignView->zoomedWidth() * (alignView->getCurrentZoom() / 100); double y = dy * alignView->zoomedHeight() * (alignView->getCurrentZoom() / 100); setPAHCorrectionOffset(static_cast(round(x)), static_cast(round(y))); } void Align::setPAHCorrectionOffset(int x, int y) { correctionOffset.setX(x); correctionOffset.setY(y); alignView->setCorrectionOffset(correctionOffset); emit newFrame(alignView); } void Align::setPAHCorrectionSelectionComplete() { pahStage = PAH_PRE_REFRESH; emit newPAHStage(pahStage); // If user stops here, we restore the settings, if not we // disable again in the refresh process // and restore when refresh is complete Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); PAHWidgets->setCurrentWidget(PAHRefreshPage); emit newPAHMessage(refreshText->text()); } void Align::setPAHSlewDone() { emit newPAHMessage("Manual slew done."); switch(pahStage) { case PAH_FIRST_ROTATE : pahStage = PAH_SECOND_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondCapturePage); appendLogText(i18n("First manual rotation done.")); break; case PAH_SECOND_ROTATE : pahStage = PAH_THIRD_CAPTURE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHThirdCapturePage); appendLogText(i18n("Second manual rotation done.")); break; default : return; // no other stage should be able to trigger this event } if (delaySpin->value() >= DELAY_THRESHOLD_NOTIFY) appendLogText(i18n("Settling...")); QTimer::singleShot(delaySpin->value(), this, &Ekos::Align::captureAndSolve); } void Align::startPAHRefreshProcess() { qCInfo(KSTARS_EKOS_ALIGN) << "Starting Polar Alignment Assistant refreshing..."; pahStage = PAH_REFRESH; emit newPAHStage(pahStage); PAHRefreshB->setEnabled(false); // Hide EQ Grids if shown if (alignView->isEQGridShown()) alignView->toggleEQGrid(); alignView->setRefreshEnabled(true); Options::setAstrometrySolverWCS(false); Options::setAutoWCS(false); // We for refresh, just capture really captureAndSolve(); } void Align::setPAHRefreshComplete() { abort(); Options::setAstrometrySolverWCS(rememberSolverWCS); Options::setAutoWCS(rememberAutoWCS); stopPAHProcess(); } void Align::processPAHStage(double orientation, double ra, double dec, double pixscale) { // Create temporary file to hold all WCS data // QTemporaryFile tmpFile(QDir::tempPath() + "/fitswcsXXXXXX"); // tmpFile.setAutoRemove(false); // tmpFile.open(); // QString newWCSFile = tmpFile.fileName(); // tmpFile.close(); QString newWCSFile = QDir::tempPath() + QString("/fitswcs%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); //alignView->setLoadWCSEnabled(true); if (pahStage == PAH_FIND_CP) { setSolverAction(GOTO_NOTHING); appendLogText( i18n("Mount is synced to celestial pole. You can now continue Polar Alignment Assistant procedure.")); pahStage = PAH_FIRST_CAPTURE; emit newPAHStage(pahStage); return; } if (pahStage == PAH_FIRST_CAPTURE) { // Set First PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Set 2nd PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); // Only invoke this if limited resource mode is false since we want to use CPU heavy WCS if (Options::limitedResourcesMode() == false) { appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { // Set Third PAH Center PAHImageInfo *solution = new PAHImageInfo(); solution->skyCenter.setRA0(alignCoord.ra0()); solution->skyCenter.setDec0(alignCoord.dec0()); solution->orientation = orientation; solution->pixelScale = pixscale; pahImageInfos.append(solution); appendLogText(i18n("Please wait while WCS data is processed...")); connect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled, Qt::UniqueConnection); alignView->createWCSFile(newWCSFile, orientation, ra, dec, pixscale); return; } } void Align::setWCSToggled(bool result) { appendLogText(i18n("WCS data processing is complete.")); //alignView->disconnect(this); disconnect(alignView, &AlignView::wcsToggled, this, &Ekos::Align::setWCSToggled); if (pahStage == PAH_FIRST_CAPTURE) { // We need WCS to be synced first if (result == false && m_wcsSynced == true) { appendLogText(i18n("WCS info is now valid. Capturing next frame...")); pahImageInfos.clear(); captureAndSolve(); return; } // Not critical error /* if (result == false) { appendLogText( i18n("Warning: failed to load WCS data in file: %1", alignView->getImageData()->getLastError())); pahStage = PAH_FIRST_ROTATE; PAHWidgets->setCurrentWidget(PAHFirstRotatePage); return; }*/ // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; bool rc = imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[0]->celestialPole = pixelPoint; // TODO check if pixelPoint is located TOO far from the current position as well // i.e. if X > Width * 2..etc if (rc == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // If celestial pole out of range, ask the user if they want to move to it if (pixelPoint.x() < (-1 * imageData->width()) || pixelPoint.x() > (imageData->width() * 2) || pixelPoint.y() < (-1 * imageData->height()) || pixelPoint.y() > (imageData->height() * 2)) { if (currentTelescope->canSync() && KMessageBox::questionYesNo( nullptr, i18n("Celestial pole is located outside of the field of view. Would you like to sync and slew " "the telescope to the celestial pole? WARNING: Slewing near poles may cause your mount to " "end up in unsafe position. Proceed with caution.")) == KMessageBox::Yes) { pahStage = PAH_FIND_CP; emit newPAHStage(pahStage); targetCoord.setRA(KStarsData::Instance()->lst()->Hours()); targetCoord.setDec(CP.dec().Degrees() > 0 ? 89.5 : -89.5); qDeleteAll(pahImageInfos); pahImageInfos.clear(); setSolverAction(GOTO_SLEW); Sync(); return; } else appendLogText(i18n("Warning: Celestial pole is located outside the field of view. Move the mount closer to the celestial pole.")); } pahStage = PAH_FIRST_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHFirstRotatePage); emit newPAHMessage(firstRotateText->text()); rotatePAH(); } else if (pahStage == PAH_SECOND_CAPTURE) { // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); FITSData *imageData = alignView->getImageData(); QPointF pixelPoint, imagePoint; imageData->wcsToPixel(CP, pixelPoint, imagePoint); pahImageInfos[1]->celestialPole = pixelPoint; pahStage = PAH_SECOND_ROTATE; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHSecondRotatePage); emit newPAHMessage(secondRotateText->text()); rotatePAH(); } else if (pahStage == PAH_THIRD_CAPTURE) { FITSData *imageData = alignView->getImageData(); // Critical error if (result == false) { appendLogText(i18n("Failed to process World Coordinate System: %1. Try again.", imageData->getLastError())); return; } // Find Celestial pole location SkyPoint CP(0, (hemisphere == NORTH_HEMISPHERE) ? 90 : -90); QPointF imagePoint; imageData->wcsToPixel(CP, celestialPolePoint, imagePoint); pahImageInfos[2]->celestialPole = celestialPolePoint; // Now find pixel locations for all recorded center coordinates in the 3rd frame reference imageData->wcsToPixel(pahImageInfos[0]->skyCenter, pahImageInfos[0]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[1]->skyCenter, pahImageInfos[1]->pixelCenter, imagePoint); imageData->wcsToPixel(pahImageInfos[2]->skyCenter, pahImageInfos[2]->pixelCenter, imagePoint); qCDebug(KSTARS_EKOS_ALIGN) << "P1 RA: " << pahImageInfos[0]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[0]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 RA: " << pahImageInfos[1]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[1]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 RA: " << pahImageInfos[2]->skyCenter.ra0().toHMSString() << "DE: " << pahImageInfos[2]->skyCenter.dec0().toDMSString(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 X: " << pahImageInfos[0]->pixelCenter.x() << "Y: " << pahImageInfos[0]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 X: " << pahImageInfos[1]->pixelCenter.x() << "Y: " << pahImageInfos[1]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 X: " << pahImageInfos[2]->pixelCenter.x() << "Y: " << pahImageInfos[2]->pixelCenter.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P1 CP X: " << pahImageInfos[0]->celestialPole.x() << "CP Y: " << pahImageInfos[0]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P2 CP X: " << pahImageInfos[1]->celestialPole.x() << "CP Y: " << pahImageInfos[1]->celestialPole.y(); qCDebug(KSTARS_EKOS_ALIGN) << "P3 CP X: " << pahImageInfos[2]->celestialPole.x() << "CP Y: " << pahImageInfos[2]->celestialPole.y(); // We have 3 points which uniquely defines a circle with its center representing the RA Axis // We have celestial pole location. So correction vector is just the vector between these two points calculatePAHError(); pahStage = PAH_STAR_SELECT; emit newPAHStage(pahStage); PAHWidgets->setCurrentWidget(PAHCorrectionPage); emit newPAHMessage(correctionText->text()); } } void Align::updateTelescopeType(int index) { if (currentCCD == nullptr) return; syncSettings(); /* bool rc = currentCCD->setTelescopeType(static_cast(index)); // If false, try to set it to existing known telescope if (rc == false) { focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; syncTelescopeInfo(); }*/ focal_length = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryFL : guideFL; aperture = (index == ISD::CCD::TELESCOPE_PRIMARY) ? primaryAperture : guideAperture; Options::setSolverScopeType(index); syncTelescopeInfo(); } // Function adapted from https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points Align::CircleSolution Align::findCircleSolutions(const QPointF &p1, const QPointF p2, double angle, QPair &circleSolutions) { QPointF solutionOne(1, 1), solutionTwo(1, 1); double radius = distance(p1, p2) / (dms::DegToRad * angle); if (p1 == p2) { if (angle == 0) { circleSolutions = qMakePair(p1, p2); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } else { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("Infinite number of solutions found.")); return INFINITE_CIRCLE_SOLUTION; } } QPointF center(p1.x() / 2 + p2.x() / 2, p1.y() / 2 + p2.y() / 2); double halfDistance = distance(center, p1); if (halfDistance > radius) { circleSolutions = qMakePair(solutionOne, solutionTwo); appendLogText(i18n("No solution is found. Points are too far away")); return NO_CIRCLE_SOLUTION; } if (halfDistance - radius == 0) { circleSolutions = qMakePair(center, solutionTwo); appendLogText(i18n("Only one solution is found.")); return ONE_CIRCLE_SOLUTION; } double root = std::hypotf(radius, halfDistance) / distance(p1, p2); solutionOne.setX(center.x() + root * (p1.y() - p2.y())); solutionOne.setY(center.y() + root * (p2.x() - p1.x())); solutionTwo.setX(center.x() - root * (p1.y() - p2.y())); solutionTwo.setY(center.y() - root * (p2.x() - p1.x())); circleSolutions = qMakePair(solutionOne, solutionTwo); return TWO_CIRCLE_SOLUTION; } double Align::distance(const QPointF &p1, const QPointF &p2) { return std::hypotf(p2.x() - p1.x(), p2.y() - p1.y()); } bool Align::findRACircle(QVector3D &RACircle) { bool rc = false; QPointF p1 = pahImageInfos[0]->pixelCenter; QPointF p2 = pahImageInfos[1]->pixelCenter; QPointF p3 = pahImageInfos[2]->pixelCenter; if (!isPerpendicular(p1, p2, p3)) rc = calcCircle(p1, p2, p3, RACircle); else if (!isPerpendicular(p1, p3, p2)) rc = calcCircle(p1, p3, p2, RACircle); else if (!isPerpendicular(p2, p1, p3)) rc = calcCircle(p2, p1, p3, RACircle); else if (!isPerpendicular(p2, p3, p1)) rc = calcCircle(p2, p3, p1, RACircle); else if (!isPerpendicular(p3, p2, p1)) rc = calcCircle(p3, p2, p1, RACircle); else if (!isPerpendicular(p3, p1, p2)) rc = calcCircle(p3, p1, p2, RACircle); else { //TRACE("\nThe three pts are perpendicular to axis\n"); return false; } return rc; } bool Align::isPerpendicular(const QPointF &p1, const QPointF &p2, const QPointF &p3) // Check the given point are perpendicular to x or y axis { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); // checking whether the line of the two pts are vertical if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { //TRACE("The points are perpendicular and parallel to x-y axis\n"); return false; } if (fabs(yDelta_a) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 1\n"); return true; } else if (fabs(yDelta_b) <= 0.0000001) { //TRACE(" A line of two point are perpendicular to x-axis 2\n"); return true; } else if (fabs(xDelta_a) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 1\n"); return true; } else if (fabs(xDelta_b) <= 0.000000001) { //TRACE(" A line of two point are perpendicular to y-axis 2\n"); return true; } else return false; } bool Align::calcCircle(const QPointF &p1, const QPointF &p2, const QPointF &p3, QVector3D &RACircle) { double yDelta_a = p2.y() - p1.y(); double xDelta_a = p2.x() - p1.x(); double yDelta_b = p3.y() - p2.y(); double xDelta_b = p3.x() - p2.x(); if (fabs(xDelta_a) <= 0.000000001 && fabs(yDelta_b) <= 0.000000001) { RACircle.setX(0.5 * (p2.x() + p3.x())); RACircle.setY(0.5 * (p1.y() + p2.y())); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } // IsPerpendicular() assure that xDelta(s) are not zero double aSlope = yDelta_a / xDelta_a; // double bSlope = yDelta_b / xDelta_b; if (fabs(aSlope - bSlope) <= 0.000000001) { // checking whether the given points are colinear. //TRACE("The three ps are colinear\n"); return false; } // calc center RACircle.setX((aSlope * bSlope * (p1.y() - p3.y()) + bSlope * (p1.x() + p2.x()) - aSlope * (p2.x() + p3.x())) / (2 * (bSlope - aSlope))); RACircle.setY(-1 * (RACircle.x() - (p1.x() + p2.x()) / 2) / aSlope + (p1.y() + p2.y()) / 2); QPointF center(RACircle.x(), RACircle.y()); RACircle.setZ(distance(center, p1)); return true; } void Align::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: solveB->setEnabled(false); loadSlewB->setEnabled(false); PAHStartB->setEnabled(false); break; default: if (state != ALIGN_PROGRESS) { solveB->setEnabled(true); if (pahStage == PAH_IDLE) { PAHStartB->setEnabled(true); loadSlewB->setEnabled(true); } } break; } } void Align::setAstrometryDevice(ISD::GDInterface *newAstrometry) { remoteParserDevice = newAstrometry; remoteSolverR->setEnabled(true); if (remoteParser.get() != nullptr) { remoteParser->setAstrometryDevice(remoteParserDevice); connect(remoteParser.get(), &AstrometryParser::solverFinished, this, &Ekos::Align::solverFinished, Qt::UniqueConnection); connect(remoteParser.get(), &AstrometryParser::solverFailed, this, &Ekos::Align::solverFailed, Qt::UniqueConnection); } } void Align::setRotator(ISD::GDInterface *newRotator) { currentRotator = newRotator; connect(currentRotator, &ISD::GDInterface::numberUpdated, this, &Ekos::Align::processNumber, Qt::UniqueConnection); } void Align::refreshAlignOptions() { if (getSolverFOV()) getSolverFOV()->setImageDisplay(Options::astrometrySolverWCS()); m_AlignTimer.setInterval(Options::astrometryTimeout() * 1000); } void Align::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { focusState = FOCUS_IDLE; filterPositionPending = false; captureAndSolve(); } } ); connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); abort(); } ); connect(filterManager.data(), &FilterManager::newStatus, [this](Ekos::FilterState filterState) { if (filterPositionPending) { switch (filterState) { case FILTER_OFFSET: appendLogText(i18n("Changing focus offset by %1 steps...", filterManager->getTargetFilterOffset())); break; case FILTER_CHANGE: appendLogText(i18n("Changing filter to %1...", FilterPosCombo->itemText(filterManager->getTargetFilterPosition() - 1))); break; case FILTER_AUTOFOCUS: appendLogText(i18n("Auto focus on filter change...")); break; default: break; } } }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { checkFilter(); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { checkFilter(); }); } QVariantMap Align::getEffectiveFOV() { KStarsData::Instance()->userdb()->GetAllEffectiveFOVs(effectiveFOVs); fov_x = fov_y = 0; for (auto &map : effectiveFOVs) { if (map["Profile"].toString() == m_ActiveProfile->name) { if (map["Width"].toInt() == ccd_width && map["Height"].toInt() == ccd_height && map["PixelW"].toDouble() == ccd_hor_pixel && map["PixelH"].toDouble() == ccd_ver_pixel && map["FocalLength"].toDouble() == focal_length) { fov_x = map["FovW"].toDouble(); fov_y = map["FovH"].toDouble(); return map; } } } return QVariantMap(); } void Align::saveNewEffectiveFOV(double newFOVW, double newFOVH) { if (newFOVW < 0 || newFOVH < 0 || (newFOVW == fov_x && newFOVH == fov_y)) return; QVariantMap effectiveMap = getEffectiveFOV(); // If ID exists, delete it first. if (effectiveMap.isEmpty() == false) KStarsData::Instance()->userdb()->DeleteEffectiveFOV(effectiveMap["id"].toString()); // If FOV is 0x0, then we just remove existing effective FOV if (newFOVW == 0.0 && newFOVH == 0.0) { calculateFOV(); return; } effectiveMap["Profile"] = m_ActiveProfile->name; effectiveMap["Width"] = ccd_width; effectiveMap["Height"] = ccd_height; effectiveMap["PixelW"] = ccd_hor_pixel; effectiveMap["PixelH"] = ccd_ver_pixel; effectiveMap["FocalLength"] = focal_length; effectiveMap["FovW"] = newFOVW; effectiveMap["FovH"] = newFOVH; KStarsData::Instance()->userdb()->AddEffectiveFOV(effectiveMap); calculateFOV(); } QStringList Align::getActiveSolvers() const { QStringList solvers; solvers << "Online"; #ifndef Q_OS_WIN solvers << "Offline"; #endif if (remoteParserDevice != nullptr) solvers << "Remote"; return solvers; } int Align::getActiveSolverIndex() const { return solverTypeGroup->checkedId(); } QString Align::getPAHMessage() const { switch (pahStage) { case PAH_IDLE: case PAH_FIND_CP: default: return introText->text(); case PAH_FIRST_CAPTURE: return firstCaptureText->text(); case PAH_FIRST_ROTATE: return firstRotateText->text(); case PAH_SECOND_CAPTURE: return secondCaptureText->text(); case PAH_SECOND_ROTATE: return secondRotateText->text(); case PAH_THIRD_CAPTURE: return thirdCaptureText->text(); case PAH_STAR_SELECT: return correctionText->text(); case PAH_PRE_REFRESH: case PAH_REFRESH: return refreshText->text(); case PAH_ERROR: return PAHErrorDescriptionLabel->text(); } } void Align::zoomAlignView() { alignView->ZoomDefault(); emit newFrame(alignView); } QJsonObject Align::getSettings() const { QJsonObject settings; settings.insert("camera", CCDCaptureCombo->currentText()); settings.insert("fw", FilterDevicesCombo->currentText()); settings.insert("filter", FilterPosCombo->currentText()); settings.insert("exp", exposureIN->value()); settings.insert("bin", binningCombo->currentIndex() + 1); settings.insert("solverAction", gotoModeButtonGroup->checkedId()); settings.insert("solverType", solverTypeGroup->checkedId()); settings.insert("scopeType", FOVScopeCombo->currentIndex()); return settings; } void Align::setSettings(const QJsonObject &settings) { CCDCaptureCombo->setCurrentText(settings["camera"].toString()); FilterDevicesCombo->setCurrentText(settings["fw"].toString()); FilterPosCombo->setCurrentText(settings["filter"].toString()); Options::setLockAlignFilterIndex(FilterPosCombo->currentIndex()); exposureIN->setValue(settings["exp"].toDouble(1)); binningCombo->setCurrentIndex(settings["bin"].toInt() - 1); gotoModeButtonGroup->button(settings["solverAction"].toInt(1))->click(); solverTypeGroup->button(settings["solverType"].toInt(1))->click(); FOVScopeCombo->setCurrentIndex(settings["scopeType"].toInt(0)); } void Align::syncSettings() { emit settingsUpdated(getSettings()); } QJsonObject Align::getPAHSettings() const { QJsonObject settings = getSettings(); settings.insert("mountDirection", PAHDirectionCombo->currentIndex()); settings.insert("mountSpeed", PAHSlewRateCombo->currentIndex()); settings.insert("mountRotation", PAHRotationSpin->value()); settings.insert("refresh", PAHExposure->value()); settings.insert("manualslew", PAHManual->isChecked()); return settings; } void Align::setPAHSettings(const QJsonObject &settings) { setSettings(settings); PAHDirectionCombo->setCurrentIndex(settings["mountDirection"].toInt(0)); PAHRotationSpin->setValue(settings["mountRotation"].toInt(30)); PAHExposure->setValue(settings["refresh"].toDouble(1)); if (settings.contains("mountSpeed")) PAHSlewRateCombo->setCurrentIndex(settings["mountSpeed"].toInt(0)); PAHManual->setChecked(settings["manualslew"].toBool(false)); } void Align::syncFOV() { QString newFOV = FOVOut->text(); QRegularExpression re("(\\d+\\.*\\d*)\\D*x\\D*(\\d+\\.*\\d*)"); QRegularExpressionMatch match = re.match(newFOV); if (match.hasMatch()) { double newFOVW = match.captured(1).toDouble(); double newFOVH = match.captured(2).toDouble(); //if (newFOVW > 0 && newFOVH > 0) saveNewEffectiveFOV(newFOVW, newFOVH); FOVOut->setStyleSheet(QString()); } else { KSNotification::error(i18n("Invalid FOV.")); FOVOut->setStyleSheet("background-color:red"); } } } diff --git a/kstars/ekos/align/offlineastrometryparser.cpp b/kstars/ekos/align/offlineastrometryparser.cpp index 52c2a7d3d..6638ef429 100644 --- a/kstars/ekos/align/offlineastrometryparser.cpp +++ b/kstars/ekos/align/offlineastrometryparser.cpp @@ -1,514 +1,383 @@ /* Astrometry.net Parser Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "offlineastrometryparser.h" #include "align.h" #include "ekos_align_debug.h" #include "ksutils.h" #include "Options.h" #include "kspaths.h" #include "ksnotification.h" namespace Ekos { OfflineAstrometryParser::OfflineAstrometryParser() : AstrometryParser() { astrometryIndex[2.8] = "index-4200"; astrometryIndex[4.0] = "index-4201"; astrometryIndex[5.6] = "index-4202"; astrometryIndex[8] = "index-4203"; astrometryIndex[11] = "index-4204"; astrometryIndex[16] = "index-4205"; astrometryIndex[22] = "index-4206"; astrometryIndex[30] = "index-4207"; astrometryIndex[42] = "index-4208"; astrometryIndex[60] = "index-4209"; astrometryIndex[85] = "index-4210"; astrometryIndex[120] = "index-4211"; astrometryIndex[170] = "index-4212"; astrometryIndex[240] = "index-4213"; astrometryIndex[340] = "index-4214"; astrometryIndex[480] = "index-4215"; astrometryIndex[680] = "index-4216"; astrometryIndex[1000] = "index-4217"; astrometryIndex[1400] = "index-4218"; astrometryIndex[2000] = "index-4219"; // Reset parity on solver failure connect(this, &OfflineAstrometryParser::solverFailed, this, [&]() { parity = QString(); }); } bool OfflineAstrometryParser::init() { -#ifdef Q_OS_OSX - if (Options::astrometryConfFileIsInternal()) - { - if(KSUtils::configureAstrometry() == false) - { - KMessageBox::information( - nullptr, - i18n( - "Failed to properly configure astrometry config file. Please click the options button in the lower right of the Astrometry Tab in Ekos to correct your settings. Then try starting Ekos again."), - i18n("Astrometry Config File Error"), "astrometry_configuration_failure_warning"); - return false; - } - } -#endif if (astrometryFilesOK) return true; if (astrometryNetOK() == false) { if (align && align->isEnabled()) KMessageBox::information( nullptr, i18n( "Failed to find astrometry.net binaries. Please click the options button in the lower right of the Astrometry Tab in Ekos to correct your settings and make sure that astrometry.net is installed. Then try starting Ekos again."), i18n("Missing astrometry files"), "missing_astrometry_binaries_warning"); return false; } astrometryFilesOK = true; QString solverPath; if (Options::astrometrySolverIsInternal()) solverPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"; else solverPath = Options::astrometrySolverBinary(); QProcess solveField; solveField.start("bash", QStringList() << "-c" << (solverPath + " --help | grep Revision")); solveField.waitForFinished(); QString output = solveField.readAllStandardOutput(); qCDebug(KSTARS_EKOS_ALIGN) << "solve-field Revision" << output; if (output.isEmpty() == false) { QString version = output.mid(9, 4); align->appendLogText(i18n("Detected Astrometry.net version %1", version)); if (version <= "0.67" && Options::astrometryUseNoFITS2FITS() == false) { Options::setAstrometryUseNoFITS2FITS(true); align->appendLogText(i18n("Setting astrometry option --no-fits2fits")); } else if (version > "0.67" && Options::astrometryUseNoFITS2FITS()) { Options::setAstrometryUseNoFITS2FITS(false); align->appendLogText(i18n("Turning off option --no-fits2fits")); } } return true; } bool OfflineAstrometryParser::astrometryNetOK() { bool solverOK = false, wcsinfoOK = false; + QFileInfo solverFileInfo; + if (Options::astrometrySolverIsInternal()) { - QFileInfo solverFileInfo(QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"); - solverOK = solverFileInfo.exists() && solverFileInfo.isFile(); + KSUtils::configureLocalAstrometryConfIfNecessary(); + #ifdef Q_OS_LINUX + solverFileInfo = QFileInfo(Options::astrometrySolverBinary()); + #else //Mac + solverFileInfo = QFileInfo(QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"); + #endif } else - { - QFileInfo solverFileInfo(Options::astrometrySolverBinary()); - solverOK = solverFileInfo.exists() && solverFileInfo.isFile(); + solverFileInfo = QFileInfo(Options::astrometrySolverBinary()); -#ifdef Q_OS_LINUX - QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); - if (QFileInfo(confPath).exists() == false) - createLocalAstrometryConf(); -#endif - } + solverOK = solverFileInfo.exists() && solverFileInfo.isFile(); if (Options::astrometryWCSIsInternal()) { QFileInfo wcsFileInfo(QCoreApplication::applicationDirPath() + "/astrometry/bin/wcsinfo"); wcsinfoOK = wcsFileInfo.exists() && wcsFileInfo.isFile(); } else { QFileInfo wcsFileInfo(Options::astrometryWCSInfo()); wcsinfoOK = wcsFileInfo.exists() && wcsFileInfo.isFile(); } return (solverOK && wcsinfoOK); } void OfflineAstrometryParser::verifyIndexFiles(double fov_x, double fov_y) { static double last_fov_x = 0, last_fov_y = 0; if (last_fov_x == fov_x && last_fov_y == fov_y) return; last_fov_x = fov_x; last_fov_y = fov_y; double fov_lower = 0.10 * fov_x; double fov_upper = fov_x; QStringList indexFiles; - QString astrometryDataDir; + QStringList astrometryDataDirs; bool indexesOK = true; -#ifdef Q_OS_OSX - if (KSUtils::getAstrometryDataDir(astrometryDataDir) == false) - return; -#else - getAstrometryDataDir(astrometryDataDir); -#endif + astrometryDataDirs = KSUtils::getAstrometryDataDirs(); QStringList nameFilter("*.fits"); - QDir directory(astrometryDataDir); - QStringList indexList = directory.entryList(nameFilter); - - // JM 2018-09-26: Also add locally stored indexes. -#ifdef Q_OS_LINUX - QDir localAstrometry(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); - indexList << localAstrometry.entryList(nameFilter); -#endif + QStringList indexList; + for(QString astrometryDataDir:astrometryDataDirs) + { + QDir directory(astrometryDataDir); + indexList << directory.entryList(nameFilter); + } QString indexSearch = indexList.join(" "); QString startIndex, lastIndex; unsigned int missingIndexes = 0; - foreach (float skymarksize, astrometryIndex.keys()) + for (float skymarksize: astrometryIndex.keys()) { if (skymarksize >= fov_lower && skymarksize <= fov_upper) { indexFiles << astrometryIndex.value(skymarksize); if (indexSearch.contains(astrometryIndex.value(skymarksize)) == false) { if (startIndex.isEmpty()) startIndex = astrometryIndex.value(skymarksize); lastIndex = astrometryIndex.value(skymarksize); indexesOK = false; missingIndexes++; } } } if (indexesOK == false) { if (missingIndexes == 1) align->appendLogText( i18n("Index file %1 is missing. Astrometry.net would not be able to adequately solve plates until you " "install the missing index files. Download the index files from http://www.astrometry.net", startIndex)); else align->appendLogText(i18n("Index files %1 to %2 are missing. Astrometry.net would not be able to " "adequately solve plates until you install the missing index files. Download the " "index files from http://www.astrometry.net", startIndex, lastIndex)); } } -bool OfflineAstrometryParser::getAstrometryDataDir(QString &dataDir) -{ - QString confPath; - - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - confPath = Options::astrometryConfFile(); - - QFile confFile(confPath); - - if (confFile.open(QIODevice::ReadOnly) == false) - { - KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " - "configuration file full path in INDI options.", - Options::astrometryConfFile())); - return false; - } - - QTextStream in(&confFile); - QString line; - while (!in.atEnd()) - { - line = in.readLine(); - if (line.isEmpty() || line.startsWith('#')) - continue; - - line = line.trimmed(); - if (line.startsWith(QLatin1String("add_path"))) - { - dataDir = line.mid(9).trimmed(); - return true; - } - } - - KSNotification::error(i18n("Unable to find data dir in astrometry configuration file.")); - return false; -} - bool OfflineAstrometryParser::startSovler(const QString &filename, const QStringList &args, bool generated) { INDI_UNUSED(generated); QStringList solverArgs = args; // Use parity if it is: 1. Already known from previous solve. 2. This is NOT a blind solve if (Options::astrometryDetectParity() && (parity.isEmpty() == false) && (args.contains("parity") == false) && (args.contains("-3") || args.contains("-L"))) solverArgs << "--parity" << parity; - QString confPath; - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - { - // JM 2018-09-26: On Linux, load the local config file. -#ifdef Q_OS_LINUX - confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); -#else - confPath = Options::astrometryConfFile(); -#endif - } + QString confPath = KSUtils::getAstrometryConfFilePath(); solverArgs << "--config" << confPath; QString solutionFile = QDir::tempPath() + "/solution.wcs"; solverArgs << "-W" << solutionFile << filename; fitsFile = filename; solver.clear(); solver = new QProcess(this); #ifdef Q_OS_OSX QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString path = env.value("PATH", ""); QString pythonExecPath = "/usr/local/opt/python/libexec/bin"; if(!Options::useDefaultPython()) pythonExecPath = Options::pythonExecPath(); if (Options::astrometrySolverIsInternal()) { env.insert("PATH", QCoreApplication::applicationDirPath() + "/netpbm/bin:" + pythonExecPath + ":/usr/local/bin:" + path); } else { env.insert("PATH", pythonExecPath + ":/usr/local/bin:" + path); } solver->setProcessEnvironment(env); if (Options::alignmentLogging()) { align->appendLogText("export PATH=" + env.value("PATH", "")); } #endif connect(solver, SIGNAL(finished(int)), this, SLOT(solverComplete(int))); solver->setProcessChannelMode(QProcess::MergedChannels); connect(solver, SIGNAL(readyReadStandardOutput()), this, SLOT(logSolver())); #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) connect(solver.data(), &QProcess::errorOccurred, this, [&]() { align->appendLogText(i18n("Error starting solver: %1", solver->errorString())); emit solverFailed(); }); #else connect(solver, SIGNAL(error(QProcess::ProcessError)), this, SIGNAL(solverFailed())); #endif solverTimer.start(); QString solverPath; if (Options::astrometrySolverIsInternal()) solverPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/solve-field"; else solverPath = Options::astrometrySolverBinary(); solver->start(solverPath, solverArgs); align->appendLogText(i18n("Starting solver...")); if (Options::alignmentLogging()) { QString command = solverPath + ' ' + solverArgs.join(' '); align->appendLogText(command); } return true; } bool OfflineAstrometryParser::stopSolver() { if (solver.isNull() == false) { solver->terminate(); solver->disconnect(); // 2019-04-25: When inparallel option is enabled in astrometry.cfg // astrometry-engine is not killed after solve-field is terminated QProcess p; p.start("killall astrometry-engine"); p.waitForFinished(); } return true; } void OfflineAstrometryParser::solverComplete(int exist_status) { solver->disconnect(); // TODO use QTemporaryFile later QString solutionFile = QDir::tempPath() + "/solution.wcs"; QFileInfo solution(solutionFile); if (exist_status != 0 || solution.exists() == false) { align->appendLogText(i18n("Solver failed. Try again.")); emit solverFailed(); return; } connect(&wcsinfo, SIGNAL(finished(int)), this, SLOT(wcsinfoComplete(int))); QString wcsPath; if (Options::astrometryWCSIsInternal()) wcsPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/wcsinfo"; else wcsPath = Options::astrometryWCSInfo(); wcsinfo.start(wcsPath, QStringList(solutionFile)); } void OfflineAstrometryParser::wcsinfoComplete(int exist_status) { wcsinfo.disconnect(); if (exist_status != 0) { align->appendLogText(i18n("WCS header missing or corrupted. Solver failed.")); emit solverFailed(); return; } QString wcsinfo_stdout = wcsinfo.readAllStandardOutput(); QStringList wcskeys = wcsinfo_stdout.split(QRegExp("[\n]")); QStringList key_value; double ra = 0, dec = 0, orientation = 0, pixscale = 0; for (auto &key : wcskeys) { key_value = key.split(' '); if (key_value.size() > 1) { if (key_value[0] == "ra_center") ra = key_value[1].toDouble(); else if (key_value[0] == "dec_center") dec = key_value[1].toDouble(); else if (key_value[0] == "orientation_center") orientation = key_value[1].toDouble(); else if (key_value[0] == "pixscale") pixscale = key_value[1].toDouble(); else if (key_value[0] == "parity") parity = (key_value[1].toInt() == 0) ? "pos" : "neg"; } } int elapsed = static_cast(round(solverTimer.elapsed() / 1000.0)); align->appendLogText(i18np("Solver completed in %1 second.", "Solver completed in %1 seconds.", elapsed)); emit solverFinished(orientation, ra, dec, pixscale); } void OfflineAstrometryParser::logSolver() { if (Options::alignmentLogging()) align->appendLogText(solver->readAll().trimmed()); } -bool OfflineAstrometryParser::createLocalAstrometryConf() -{ - bool rc = false; - - QString confPath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + QLatin1Literal("/astrometry.cfg"); - QString systemConfPath = Options::astrometryConfFile(); - - // Check if directory already exists, if it doesn't create one - QDir writableDir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); - if (writableDir.exists() == false) - { - rc = writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); - - if (rc == false) - { - qCCritical(KSTARS_EKOS_ALIGN) << "Failed to create local astrometry directory"; - return false; - } - } - - // Now copy system astrometry.cfg to local directory - rc = QFile(systemConfPath).copy(confPath); - - if (rc == false) - { - qCCritical(KSTARS_EKOS_ALIGN) << "Failed to copy" << systemConfPath << "to" << confPath; - return false; - } - - QFile localConf(confPath); - - // Open file and add our own path to it - if (localConf.open(QFile::ReadWrite)) - { - QString all = localConf.readAll(); - QStringList lines = all.split("\n"); - for (int i = 0; i < lines.count(); i++) - { - if (lines[i].startsWith("add_path")) - { - lines.insert(i + 1, QString("add_path %1astrometry").arg(KSPaths::writableLocation(QStandardPaths::GenericDataLocation))); - break; - } - } - - // Clear contents - localConf.resize(0); - - // Now write back all the lines including our own inserted above - QTextStream out(&localConf); - for(const QString &line : lines) - out << line << endl; - localConf.close(); - return true; - } - - qCCritical(KSTARS_EKOS_ALIGN) << "Failed to open local astrometry config" << confPath; - return false; -} } diff --git a/kstars/ekos/align/offlineastrometryparser.h b/kstars/ekos/align/offlineastrometryparser.h index 5a875d6cb..59567202a 100644 --- a/kstars/ekos/align/offlineastrometryparser.h +++ b/kstars/ekos/align/offlineastrometryparser.h @@ -1,63 +1,61 @@ /* Astrometry.net Parser Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "astrometryparser.h" #include #include #include #include namespace Ekos { class Align; /** * @class OfflineAstrometryParser * OfflineAstrometryParser invokes the offline astrometry.net solver to find solutions to captured images. * * @author Jasem Mutlaq */ class OfflineAstrometryParser : public AstrometryParser { Q_OBJECT public: OfflineAstrometryParser(); virtual ~OfflineAstrometryParser() override = default; virtual void setAlign(Align *_align) override { align = _align; } virtual bool init() override; virtual void verifyIndexFiles(double fov_x, double fov_y) override; virtual bool startSovler(const QString &filename, const QStringList &args, bool generated = true) override; virtual bool stopSolver() override; public slots: void solverComplete(int exist_status); void wcsinfoComplete(int exist_status); void logSolver(); private: bool astrometryNetOK(); - bool createLocalAstrometryConf(); - bool getAstrometryDataDir(QString &dataDir); QMap astrometryIndex; QString parity; QPointer solver; QProcess wcsinfo; QTime solverTimer; QString fitsFile; bool astrometryFilesOK { false }; Align *align { nullptr }; }; } diff --git a/kstars/ekos/align/opsalign.cpp b/kstars/ekos/align/opsalign.cpp index a4d78eb49..8970233c4 100644 --- a/kstars/ekos/align/opsalign.cpp +++ b/kstars/ekos/align/opsalign.cpp @@ -1,197 +1,197 @@ /* Astrometry.net Options Editor Copyright (C) 2017 Jasem Mutlaq Copyright (C) 2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "opsalign.h" #include "align.h" #include "fov.h" #include "kstars.h" #include "ksnotification.h" #include "Options.h" #include #include namespace Ekos { OpsAlign::OpsAlign(Align *parent) : QWidget(KStars::Instance()) { setupUi(this); alignModule = parent; //Get a pointer to the KConfigDialog m_ConfigDialog = KConfigDialog::exists("alignsettings"); connect(m_ConfigDialog->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); connect(m_ConfigDialog->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(slotApply())); connect(SetupPython, SIGNAL(clicked()), this, SLOT(setupPython())); -#ifdef Q_OS_OSX - connect(kcfg_AstrometrySolverIsInternal, SIGNAL(clicked()), this, SLOT(toggleSolverInternal())); - kcfg_AstrometrySolverIsInternal->setToolTip(i18n("Internal or External Plate Solver?")); - if (Options::astrometrySolverIsInternal()) - kcfg_AstrometrySolverBinary->setEnabled(false); connect(kcfg_AstrometryConfFileIsInternal, SIGNAL(clicked()), this, SLOT(toggleConfigInternal())); kcfg_AstrometryConfFileIsInternal->setToolTip(i18n("Internal or External astrometry.cfg?")); if (Options::astrometryConfFileIsInternal()) kcfg_AstrometryConfFile->setEnabled(false); +#ifdef Q_OS_OSX + connect(kcfg_AstrometrySolverIsInternal, SIGNAL(clicked()), this, SLOT(toggleSolverInternal())); + kcfg_AstrometrySolverIsInternal->setToolTip(i18n("Internal or External Plate Solver?")); + if (Options::astrometrySolverIsInternal()) + kcfg_AstrometrySolverBinary->setEnabled(false); + connect(kcfg_AstrometryWCSIsInternal, SIGNAL(clicked()), this, SLOT(toggleWCSInternal())); kcfg_AstrometryWCSIsInternal->setToolTip(i18n("Internal or External wcsinfo?")); if (Options::astrometryWCSIsInternal()) kcfg_AstrometryWCSInfo->setEnabled(false); connect(kcfg_UseDefaultPython, SIGNAL(clicked()), this, SLOT(togglePythonDefault())); kcfg_PythonExecPath->setVisible(!Options::useDefaultPython()); SetupPython->setVisible(Options::useDefaultPython()); #else kcfg_AstrometrySolverIsInternal->setVisible(false); - kcfg_AstrometryConfFileIsInternal->setVisible(false); kcfg_AstrometryWCSIsInternal->setVisible(false); kcfg_UseDefaultPython->setVisible(false); pythonLabel->setVisible(false); SetupPython->setVisible(false); kcfg_PythonExecPath->setVisible(false); #endif #ifdef Q_OS_WIN kcfg_AstrometrySolverBinary->setEnabled(false); kcfg_AstrometryWCSInfo->setEnabled(false); kcfg_AstrometryConfFile->setEnabled(false); #endif } void OpsAlign::setupPython() { if(brewInstalled() && pythonInstalled() && astropyInstalled()) { KSNotification::info(i18n("Homebrew, python, and astropy are already installed")); return; } if (KMessageBox::questionYesNo(nullptr, i18n("This installer will install the following requirements for astrometry.net if they are not installed:\nHomebrew -an OS X Unix Program Package Manager\nPython3 -A Powerful Scripting Language \nAstropy -Python Modules for Astronomy \n Do you wish to continue?"), i18n("Install and Configure Python")) == KMessageBox::Yes) { QProcess* install = new QProcess(this); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString path = env.value("PATH", ""); env.insert("PATH", "/usr/local/opt/python/libexec/bin:/usr/local/bin:" + path); install->setProcessEnvironment(env); if(!brewInstalled()) { KSNotification::info(i18n("Homebrew is not installed. \nA Terminal window will pop up for you to install Homebrew. \n When you are all done, then you can close the Terminal and click the setup button again.")); QStringList installArgs; QString homebrewInstallScript = "tell application \"Terminal\"\n" " do script \"/usr/bin/ruby -e \\\"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\\\"\"\n" "end tell\n"; QString bringToFront = "tell application \"Terminal\"\n" " activate\n" "end tell\n"; QStringList processArguments; processArguments << "-l" << "AppleScript"; install->start("/usr/bin/osascript", processArguments); install->write(homebrewInstallScript.toUtf8()); install->write(bringToFront.toUtf8()); install->closeWriteChannel(); install->waitForFinished(); return; } if(!pythonInstalled()) { KSNotification::info(i18n("Homebrew installed \nPython3 will install when you click Ok \nAstropy waiting . . . \n (Note: this might take a few minutes, please be patient.)")); install->start("/usr/local/bin/brew", QStringList() << "install" << "python3"); install->waitForFinished(); if(!pythonInstalled()) { KSNotification::info(i18n("Python install failure")); return; } } if(!astropyInstalled()) { KSNotification::info(i18n("Homebrew installed \nPython3 installed \nAstropy will install when you click Ok \n (Note: this might take a few minutes, please be patient.)")); install->start("/usr/local/bin/pip3", QStringList() << "install" << "astropy"); install->waitForFinished(); if(!astropyInstalled()) { KSNotification::info(i18n("Astropy install failure")); return; } } KSNotification::info(i18n("All installations are complete and ready to use.")); } } bool OpsAlign::brewInstalled() { return QFileInfo("/usr/local/bin/brew").exists(); } bool OpsAlign::pythonInstalled() { return QFileInfo("/usr/local/bin/python3").exists(); } bool OpsAlign::astropyInstalled() { QProcess testAstropy; testAstropy.start("/usr/local/bin/pip3 list"); testAstropy.waitForFinished(); QString listPip(testAstropy.readAllStandardOutput()); qDebug() << listPip; return listPip.contains("astropy", Qt::CaseInsensitive); } void OpsAlign::toggleSolverInternal() { kcfg_AstrometrySolverBinary->setEnabled(!kcfg_AstrometrySolverIsInternal->isChecked()); if (kcfg_AstrometrySolverIsInternal->isChecked()) kcfg_AstrometrySolverBinary->setText("*Internal Solver*"); else kcfg_AstrometrySolverBinary->setText(KSUtils::getDefaultPath("AstrometrySolverBinary")); } void OpsAlign::toggleConfigInternal() { kcfg_AstrometryConfFile->setEnabled(!kcfg_AstrometryConfFileIsInternal->isChecked()); if (kcfg_AstrometryConfFileIsInternal->isChecked()) kcfg_AstrometryConfFile->setText("*Internal astrometry.cfg*"); else kcfg_AstrometryConfFile->setText(KSUtils::getDefaultPath("AstrometryConfFile")); } void OpsAlign::toggleWCSInternal() { kcfg_AstrometryWCSInfo->setEnabled(!kcfg_AstrometryWCSIsInternal->isChecked()); if (kcfg_AstrometryWCSIsInternal->isChecked()) kcfg_AstrometryWCSInfo->setText("*Internal wcsinfo*"); else kcfg_AstrometryWCSInfo->setText(KSUtils::getDefaultPath("AstrometryWCSInfo")); } void OpsAlign::togglePythonDefault() { bool defaultPython = kcfg_UseDefaultPython->isChecked(); kcfg_PythonExecPath->setVisible(!defaultPython); SetupPython->setVisible(defaultPython); } void OpsAlign::slotApply() { emit settingsUpdated(); } } diff --git a/kstars/ekos/align/opsastrometrycfg.cpp b/kstars/ekos/align/opsastrometrycfg.cpp index 935e1da1e..dcc1316f4 100644 --- a/kstars/ekos/align/opsastrometrycfg.cpp +++ b/kstars/ekos/align/opsastrometrycfg.cpp @@ -1,108 +1,147 @@ #include "opsastrometrycfg.h" #include "align.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #include "ksnotification.h" #include "ui_opsastrometrycfg.h" +#include "kspaths.h" #include #include +#include namespace Ekos { OpsAstrometryCfg::OpsAstrometryCfg(Align *parent) : QDialog(KStars::Instance()) { setupUi(this); alignModule = parent; //Get a pointer to the KConfigDialog m_ConfigDialog = KConfigDialog::exists("alignsettings"); connect(m_ConfigDialog->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); connect(m_ConfigDialog->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(slotApply())); connect(astrometryCFGDisplay, SIGNAL(textChanged()), SLOT(slotCFGEditorUpdated())); connect(loadCFG, SIGNAL(clicked()), this, SLOT(slotLoadCFG())); - connect(setIndexFileB, SIGNAL(clicked()), this, SLOT(slotSetAstrometryIndexFileLocation())); + connect(addIndexFilePath, SIGNAL(clicked()), this, SLOT(slotAddAstrometryIndexFileLocation())); + connect(removeIndexFilePath, SIGNAL(clicked()), this, SLOT(slotRemoveAstrometryIndexFileLocation())); + + connect(AstrometryIndexFileLocations, &QListWidget::itemDoubleClicked, this, &OpsAstrometryCfg::slotClickAstrometryIndexFileLocation); slotLoadCFG(); } -void OpsAstrometryCfg::slotLoadCFG() +void OpsAstrometryCfg::showEvent(QShowEvent *) { - QString confPath; + slotLoadCFG(); +} - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - confPath = Options::astrometryConfFile(); +void OpsAstrometryCfg::slotLoadCFG() +{ + QString confPath = KSUtils::getAstrometryConfFilePath(); QFile confFile(confPath); astrometryCFGLocation->setText(confPath); if (confFile.open(QIODevice::ReadOnly) == false) { - KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " - "configuration file full path in INDI options.", - Options::astrometryConfFile())); - return; + bool confFileExists = false; + if(Options::astrometryConfFileIsInternal()) + { + if(KSUtils::configureLocalAstrometryConfIfNecessary()) + { + if (confFile.open(QIODevice::ReadOnly)) + confFileExists = true; + } + } + if(!confFileExists) + { + KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " + "configuration file full path in INDI options.", + confPath)); + return; + } } QTextStream in(&confFile); currentCFGText = in.readAll(); astrometryCFGDisplay->setPlainText(currentCFGText); confFile.close(); + AstrometryIndexFileLocations->clear(); + QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs(); + for(QString astrometryDataDir:astrometryDataDirs) + { + QListWidgetItem *item = new QListWidgetItem(astrometryDataDir); + item->setIcon(QIcon::fromTheme("stock_folder")); + AstrometryIndexFileLocations->addItem(item); + } } -void OpsAstrometryCfg::slotSetAstrometryIndexFileLocation() +void OpsAstrometryCfg::slotAddAstrometryIndexFileLocation() { -#ifdef Q_OS_OSX - KSUtils::setAstrometryDataDir(kcfg_AstrometryIndexFileLocation->text()); -#endif + QString dir = + QFileDialog::getExistingDirectory(KStars::Instance(), i18n("Index File Directory"), QDir::homePath()); + + if (dir.isEmpty()) + return; + + KSUtils::addAstrometryDataDir(dir); + slotLoadCFG(); +} + +void OpsAstrometryCfg::slotRemoveAstrometryIndexFileLocation() +{ + if(AstrometryIndexFileLocations->selectedItems().count() == 0) + { + KSNotification::error(i18n("Please select an Index Path to remove first.")); + return; + } + KSUtils::removeAstrometryDataDir(AstrometryIndexFileLocations->selectedItems().first()->text()); slotLoadCFG(); } +void OpsAstrometryCfg::slotClickAstrometryIndexFileLocation(QListWidgetItem *item) +{ + if(AstrometryIndexFileLocations->count()==0) + return; + QUrl path = QUrl::fromLocalFile(item->text()); + QDesktopServices::openUrl(path); +} + void OpsAstrometryCfg::slotApply() { if (currentCFGText != astrometryCFGDisplay->toPlainText()) { - QString confPath; - - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - confPath = Options::astrometryConfFile(); + QString confPath = KSUtils::getAstrometryConfFilePath(); QFile confFile(confPath); if (confFile.open(QIODevice::WriteOnly) == false) KSNotification::error(i18n("Internal Astrometry configuration file write error.")); else { QTextStream out(&confFile); out << astrometryCFGDisplay->toPlainText(); confFile.close(); KSNotification::info(i18n("Astrometry.cfg successfully saved.")); currentCFGText = astrometryCFGDisplay->toPlainText(); - QString astrometryDataDir; -#ifdef Q_OS_OSX - KSUtils::getAstrometryDataDir(astrometryDataDir); -#endif - if(astrometryDataDir != kcfg_AstrometryIndexFileLocation->text()) - kcfg_AstrometryIndexFileLocation->setText(astrometryDataDir); + AstrometryIndexFileLocations->clear(); + AstrometryIndexFileLocations->addItems(KSUtils::getAstrometryDataDirs()); } } } void OpsAstrometryCfg::slotCFGEditorUpdated() { if (currentCFGText != astrometryCFGDisplay->toPlainText()) m_ConfigDialog->button(QDialogButtonBox::Apply)->setEnabled(true); } } diff --git a/kstars/ekos/align/opsastrometrycfg.h b/kstars/ekos/align/opsastrometrycfg.h index b52c95c7d..20f000a02 100644 --- a/kstars/ekos/align/opsastrometrycfg.h +++ b/kstars/ekos/align/opsastrometrycfg.h @@ -1,33 +1,38 @@ #pragma once #include "ui_opsastrometrycfg.h" #include class KConfigDialog; namespace Ekos { class Align; class OpsAstrometryCfg : public QDialog, public Ui::OpsAstrometryCfg { Q_OBJECT public: explicit OpsAstrometryCfg(Align *parent); virtual ~OpsAstrometryCfg() override = default; + protected: + void showEvent(QShowEvent *) override; + private slots: void slotLoadCFG(); - void slotSetAstrometryIndexFileLocation(); + void slotAddAstrometryIndexFileLocation(); + void slotRemoveAstrometryIndexFileLocation(); + void slotClickAstrometryIndexFileLocation(QListWidgetItem *item); void slotApply(); void slotCFGEditorUpdated(); private: KConfigDialog *m_ConfigDialog { nullptr }; Align *alignModule { nullptr }; QString currentCFGText; }; } diff --git a/kstars/ekos/align/opsastrometrycfg.ui b/kstars/ekos/align/opsastrometrycfg.ui index 205a5ac86..e0a6e484e 100644 --- a/kstars/ekos/align/opsastrometrycfg.ui +++ b/kstars/ekos/align/opsastrometrycfg.ui @@ -1,102 +1,127 @@ OpsAstrometryCfg 0 0 - 400 + 407 300 Dialog <html><head/><body><p>This is the absolute location of the Astrometry.cfg file on the filesystem.</p></body></html> Astrometry.cfg Location: false false <html><head/><body><p>This is the absolute location of the Astrometry.cfg file on the filesystem.</p></body></html> <html><head/><body><p>This button will let you reload the Astrometry.cfg file in the event that something was changed outside of KStars.</p></body></html> Reload <html><head/><body><p>This is the absolute location of the Astrometry.cfg file on the filesystem.</p></body></html> - Index File Location: + Index File Locations: - - - true + + + + 16777215 + 70 + - - false + + + + + + + 32 + 32 + - - <html><head/><body><p>This is the absolute location of the Astrometry.cfg file on the filesystem.</p></body></html> + + + 32 + 32 + + + + + - - - <html><head/><body><p>This button will let you reload the Astrometry.cfg file in the event that something was changed outside of KStars.</p></body></html> + + + + 32 + 32 + + + + + 32 + 32 + - Set + - <html><head/><body><p>In this space you can edit the Astrometry.cfg file. When you finish, you can hit &quot;Apply&quot; or &quot;Ok&quot; to save your changes. </p></body></html> diff --git a/kstars/ekos/align/opsastrometryindexfiles.cpp b/kstars/ekos/align/opsastrometryindexfiles.cpp index 9f2aa70c0..ba1c43c29 100644 --- a/kstars/ekos/align/opsastrometryindexfiles.cpp +++ b/kstars/ekos/align/opsastrometryindexfiles.cpp @@ -1,541 +1,551 @@ #include "opsastrometryindexfiles.h" #include "align.h" #include "kstars.h" +#include "ksutils.h" #include "Options.h" #include "kspaths.h" #include "ksnotification.h" #include #include namespace Ekos { OpsAstrometryIndexFiles::OpsAstrometryIndexFiles(Align *parent) : QDialog(KStars::Instance()) { setupUi(this); downloadSpeed = 100; actualdownloadSpeed = downloadSpeed; alignModule = parent; manager = new QNetworkAccessManager(); //Get a pointer to the KConfigDialog // m_ConfigDialog = KConfigDialog::exists( "alignsettings" ); connect(openIndexFileDirectory, SIGNAL(clicked()), this, SLOT(slotOpenIndexFileDirectory())); astrometryIndex[2.8] = "00"; astrometryIndex[4.0] = "01"; astrometryIndex[5.6] = "02"; astrometryIndex[8] = "03"; astrometryIndex[11] = "04"; astrometryIndex[16] = "05"; astrometryIndex[22] = "06"; astrometryIndex[30] = "07"; astrometryIndex[42] = "08"; astrometryIndex[60] = "09"; astrometryIndex[85] = "10"; astrometryIndex[120] = "11"; astrometryIndex[170] = "12"; astrometryIndex[240] = "13"; astrometryIndex[340] = "14"; astrometryIndex[480] = "15"; astrometryIndex[680] = "16"; astrometryIndex[1000] = "17"; astrometryIndex[1400] = "18"; astrometryIndex[2000] = "19"; QList checkboxes = findChildren(); + connect(indexLocations,static_cast(&QComboBox::currentIndexChanged), this, &OpsAstrometryIndexFiles::slotUpdate); + for (auto &checkBox : checkboxes) { - connect(checkBox, SIGNAL(clicked(bool)), this, SLOT(downloadOrDeleteIndexFiles(bool))); + connect(checkBox, &QCheckBox::clicked, this,&OpsAstrometryIndexFiles::downloadOrDeleteIndexFiles); } QList progressBars = findChildren(); QList qLabels = findChildren(); QList qButtons = findChildren(); for (auto &bar : progressBars) { if(bar->objectName().contains("progress")) { bar->setVisible(false); bar->setTextVisible(false); } } for (auto &button : qButtons) { if(button->objectName().contains("cancel")) { button->setVisible(false); } } for (QLabel * label : qLabels) { if(label->text().contains("info") || label->text().contains("perc")) { label->setVisible(false); } } } void OpsAstrometryIndexFiles::showEvent(QShowEvent *) { + QStringList astrometryDataDirs =KSUtils::getAstrometryDataDirs(); + + if (astrometryDataDirs.count() == 0) + return; + indexLocations->clear(); + if(astrometryDataDirs.count()>1) + indexLocations->addItem("All Sources"); + indexLocations->addItems(astrometryDataDirs); slotUpdate(); } void OpsAstrometryIndexFiles::slotUpdate() { + QList checkboxes = findChildren(); + + for (auto &checkBox : checkboxes) + { + checkBox->setChecked(false); + } + + if(indexLocations->count()==0) + return; + double fov_w, fov_h, fov_pixscale; // Values in arcmins. Scale in arcsec per pixel alignModule->getFOVScale(fov_w, fov_h, fov_pixscale); double fov_check = qMax(fov_w, fov_h); FOVOut->setText(QString("%1' x %2'").arg(QString::number(fov_w, 'f', 2), QString::number(fov_h, 'f', 2))); - QString astrometryDataDir; + QStringList nameFilter("*.fits"); - if (getAstrometryDataDir(astrometryDataDir) == false) - return; + if (Options::astrometrySolverIsInternal()) + KSUtils::configureLocalAstrometryConfIfNecessary(); - indexLocation->setText(astrometryDataDir); + QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();; - QStringList nameFilter("*.fits"); - QDir directory(astrometryDataDir); - QStringList indexList = directory.entryList(nameFilter); + bool allDirsSelected = (indexLocations->currentIndex() == 0 && astrometryDataDirs.count() > 1); + bool folderIsWriteable; - // JM 2018-09-26: Also add locally stored indexes. -#ifdef Q_OS_LINUX - QDir localAstrometry(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry")); - indexList << localAstrometry.entryList(nameFilter); -#endif + QStringList astrometryDataDirsToIndex; - for (auto &indexName : indexList) + if(allDirsSelected) { -#ifdef Q_OS_LINUX - if (fileCountMatches(directory, indexName) || fileCountMatches(localAstrometry, indexName)) -#else - if (fileCountMatches(directory, indexName)) -#endif + folderDetails->setText(i18n("Downloads Disabled, this is not a directory, it is a list of all index files.")); + folderIsWriteable = false; + astrometryDataDirsToIndex = astrometryDataDirs; + openIndexFileDirectory->setEnabled(false); + } + else + { + QString folderPath = indexLocations->currentText(); + folderIsWriteable = QFileInfo(folderPath).isWritable(); + if(folderIsWriteable) + folderDetails->setText(i18n("Downloads Enabled, the directory exists and is writeable.")); + else + folderDetails->setText(i18n("Downloads Disabled, directory permissions issue.")); + if(!QFileInfo(folderPath).exists()) + folderDetails->setText(i18n("Downloads Disabled, directory does not exist.")); + astrometryDataDirsToIndex << folderPath; + openIndexFileDirectory->setEnabled(true); + } + folderDetails->setCursorPosition(0); + + //This loop checks all the folders that are supposed to be checked for the files + //It checks the box if it finds them + for(QString astrometryDataDir:astrometryDataDirsToIndex) + { + QDir directory(astrometryDataDir); + QStringList indexList = directory.entryList(nameFilter); + for (auto &indexName : indexList) { - indexName = indexName.replace('-', '_').left(10); - QCheckBox *indexCheckBox = findChild(indexName); - if (indexCheckBox) - indexCheckBox->setChecked(true); + if (fileCountMatches(directory, indexName)) + { + indexName = indexName.replace('-', '_').left(10); + QCheckBox *indexCheckBox = findChild(indexName); + if (indexCheckBox) + indexCheckBox->setChecked(true); + } } } - QList checkboxes = findChildren(); + for (auto &checkBox : checkboxes) { + checkBox->setEnabled(folderIsWriteable); checkBox->setIcon(QIcon(":/icons/astrometry-optional.svg")); checkBox->setToolTip(i18n("Optional")); + checkBox->setStyleSheet(""); } float last_skymarksize = 2; for (auto &skymarksize : astrometryIndex.keys()) { if ((skymarksize >= 0.40 * fov_check && skymarksize <= 0.9 * fov_check) || (fov_check > last_skymarksize && fov_check < skymarksize)) { QString indexName1 = "index_41" + astrometryIndex.value(skymarksize); QString indexName2 = "index_42" + astrometryIndex.value(skymarksize); QCheckBox *indexCheckBox1 = findChild(indexName1); QCheckBox *indexCheckBox2 = findChild(indexName2); if (indexCheckBox1) { indexCheckBox1->setIcon(QIcon(":/icons/astrometry-required.svg")); indexCheckBox1->setToolTip(i18n("Required")); } if (indexCheckBox2) { indexCheckBox2->setIcon(QIcon(":/icons/astrometry-required.svg")); indexCheckBox2->setToolTip(i18n("Required")); } } else if (skymarksize >= 0.10 * fov_check && skymarksize <= fov_check) { QString indexName1 = "index_41" + astrometryIndex.value(skymarksize); QString indexName2 = "index_42" + astrometryIndex.value(skymarksize); QCheckBox *indexCheckBox1 = findChild(indexName1); QCheckBox *indexCheckBox2 = findChild(indexName2); if (indexCheckBox1) { indexCheckBox1->setIcon(QIcon(":/icons/astrometry-recommended.svg")); indexCheckBox1->setToolTip(i18n("Recommended")); } if (indexCheckBox2) { indexCheckBox2->setIcon(QIcon(":/icons/astrometry-recommended.svg")); indexCheckBox2->setToolTip(i18n("Recommended")); } } last_skymarksize = skymarksize; } + + //This loop goes over all the directories and adds a stylesheet to change the look of the checkbox text + //if the File is installed in any directory. Note that this indicator is then used below in the + //Index File download function to check if they really want to do install a file that is installed. + for(QString astrometryDataDir:astrometryDataDirs) + { + QDir directory(astrometryDataDir); + QStringList indexList = directory.entryList(nameFilter); + + for (auto &indexName : indexList) + { + if (fileCountMatches(directory, indexName)) + { + indexName = indexName.replace('-', '_').left(10); + QCheckBox *indexCheckBox = findChild(indexName); + if (indexCheckBox) + indexCheckBox->setStyleSheet("QCheckBox{font-weight: bold; color:green}"); + } + } + } } bool OpsAstrometryIndexFiles::fileCountMatches(QDir directory, QString indexName) { QString indexNameMatch = indexName.left(10) + "*.fits"; QStringList list = directory.entryList(QStringList(indexNameMatch)); int count = 0; if(indexName.contains("4207") || indexName.contains("4206") || indexName.contains("4205")) count = 12; else if(indexName.contains("4204") || indexName.contains("4203") || indexName.contains("4202") || indexName.contains("4201") || indexName.contains("4200")) count = 48; else count = 1; return list.count() == count; } void OpsAstrometryIndexFiles::slotOpenIndexFileDirectory() { - QString astrometryDataDir; - if (getAstrometryDataDir(astrometryDataDir) == false) + if(indexLocations->count()==0) return; - QUrl path = QUrl::fromLocalFile(astrometryDataDir); + QUrl path = QUrl::fromLocalFile(indexLocations->currentText()); QDesktopServices::openUrl(path); } -bool OpsAstrometryIndexFiles::getAstrometryDataDir(QString &dataDir) -{ - QString confPath; - - if (Options::astrometryConfFileIsInternal()) - confPath = QCoreApplication::applicationDirPath() + "/astrometry/bin/astrometry.cfg"; - else - confPath = Options::astrometryConfFile(); - - QFile confFile(confPath); - - if (confFile.open(QIODevice::ReadOnly) == false) - { - KSNotification::error(i18n("Astrometry configuration file corrupted or missing: %1\nPlease set the " - "configuration file full path in INDI options.", - Options::astrometryConfFile())); - return false; - } - - QTextStream in(&confFile); - QString line; - while (!in.atEnd()) - { - line = in.readLine(); - if (line.isEmpty() || line.startsWith('#')) - continue; - - line = line.trimmed(); - if (line.startsWith(QLatin1String("add_path"))) - { - dataDir = line.mid(9).trimmed(); - return true; - } - } - - KSNotification::error(i18n("Unable to find data dir in astrometry configuration file.")); - return false; -} - bool OpsAstrometryIndexFiles::astrometryIndicesAreAvailable() { QNetworkReply *response = manager->get(QNetworkRequest(QUrl("http://broiler.astrometry.net"))); QTimer timeout(this); timeout.setInterval(5000); timeout.setSingleShot(true); timeout.start(); while (!response->isFinished()) { if (!timeout.isActive()) { response->deleteLater(); return false; } qApp->processEvents(); } timeout.stop(); bool wasSuccessful = (response->error() == QNetworkReply::NoError); response->deleteLater(); return wasSuccessful; } void OpsAstrometryIndexFiles::downloadIndexFile(const QString &URL, const QString &fileN, QCheckBox *checkBox, int currentIndex, int maxIndex, double fileSize) { QTime downloadTime; downloadTime.start(); QString indexString = QString::number(currentIndex); if (currentIndex < 10) indexString = '0' + indexString; QString indexSeriesName = checkBox->text().remove('&'); QProgressBar *indexDownloadProgress = findChild(indexSeriesName.replace('-', '_').left(10) + "_progress"); QLabel *indexDownloadInfo = findChild(indexSeriesName.replace('-', '_').left(10) + "_info"); QPushButton *indexDownloadCancel = findChild(indexSeriesName.replace('-', '_').left(10) + "_cancel"); QLabel *indexDownloadPerc = findChild(indexSeriesName.replace('-', '_').left(10) + "_perc"); setDownloadInfoVisible(indexSeriesName, checkBox, true); if(indexDownloadInfo) { if (indexDownloadProgress && maxIndex > 0) indexDownloadProgress->setValue(currentIndex * 100 / maxIndex); indexDownloadInfo->setText("(" + QString::number(currentIndex) + '/' + QString::number(maxIndex + 1) + ") "); } QString indexURL = URL; indexURL.replace('*', indexString); QNetworkReply *response = manager->get(QNetworkRequest(QUrl(indexURL))); //Shut it down after too much time elapses. //If the filesize is less than 4 MB, it sets the timeout for 1 minute or 60000 ms. //If it's larger, it assumes a bad download rate of 1 Mbps (100 bytes/ms) //and the calculation estimates the time in milliseconds it would take to download. int timeout = 60000; if(fileSize > 4000000) timeout = fileSize / downloadSpeed; //qDebug()<<"Filesize: "<< fileSize << ", timeout: " << timeout; QMetaObject::Connection *cancelConnection = new QMetaObject::Connection(); QMetaObject::Connection *replyConnection = new QMetaObject::Connection(); QMetaObject::Connection *percentConnection = new QMetaObject::Connection(); if(indexDownloadPerc) { *percentConnection = connect(response, &QNetworkReply::downloadProgress, [ = ](qint64 bytesReceived, qint64 bytesTotal) { if (indexDownloadProgress) { indexDownloadProgress->setValue(bytesReceived); indexDownloadProgress->setMaximum(bytesTotal); } indexDownloadPerc->setText(QString::number(bytesReceived * 100 / bytesTotal) + '%'); }); } timeoutTimer.disconnect(); connect(&timeoutTimer, &QTimer::timeout, [&]() { KSNotification::error(i18n("Download Timed out. Either the network is not fast enough, the file is not accessible, or you are not connected.")); disconnectDownload(cancelConnection, replyConnection, percentConnection); if(response) { response->abort(); response->deleteLater(); } setDownloadInfoVisible(indexSeriesName, checkBox, false); }); timeoutTimer.start(timeout); *cancelConnection = connect(indexDownloadCancel, &QPushButton::clicked, [ = ]() { qDebug() << "Download Cancelled."; timeoutTimer.stop(); disconnectDownload(cancelConnection, replyConnection, percentConnection); if(response) { response->abort(); response->deleteLater(); } setDownloadInfoVisible(indexSeriesName, checkBox, false); }); *replyConnection = connect(response, &QNetworkReply::finished, this, [ = ]() { timeoutTimer.stop(); if(response) { disconnectDownload(cancelConnection, replyConnection, percentConnection); setDownloadInfoVisible(indexSeriesName, checkBox, false); response->deleteLater(); if (response->error() != QNetworkReply::NoError) return; QByteArray responseData = response->readAll(); QString indexFileN = fileN; indexFileN.replace('*', indexString); QFile file(indexFileN); if (QFileInfo(QFileInfo(file).path()).isWritable()) { if (!file.open(QIODevice::WriteOnly)) { KSNotification::error(i18n("File Write Error")); slotUpdate(); return; } else { file.write(responseData.data(), responseData.size()); file.close(); int downloadedFileSize = QFileInfo(file).size(); int dtime = downloadTime.elapsed(); actualdownloadSpeed = (actualdownloadSpeed + (downloadedFileSize / dtime)) / 2; qDebug() << "Filesize: " << downloadedFileSize << ", time: " << dtime << ", inst speed: " << downloadedFileSize / dtime << ", averaged speed: " << actualdownloadSpeed; } } else { KSNotification::error(i18n("Astrometry Folder Permissions Error")); } if (currentIndex == maxIndex) { slotUpdate(); } else downloadIndexFile(URL, fileN, checkBox, currentIndex + 1, maxIndex, fileSize); } }); } void OpsAstrometryIndexFiles::setDownloadInfoVisible(QString indexSeriesName, QCheckBox *checkBox, bool set) { QProgressBar *indexDownloadProgress = findChild(indexSeriesName.replace('-', '_').left(10) + "_progress"); QLabel *indexDownloadInfo = findChild(indexSeriesName.replace('-', '_').left(10) + "_info"); QPushButton *indexDownloadCancel = findChild(indexSeriesName.replace('-', '_').left(10) + "_cancel"); QLabel *indexDownloadPerc = findChild(indexSeriesName.replace('-', '_').left(10) + "_perc"); if (indexDownloadProgress) indexDownloadProgress->setVisible(set); if (indexDownloadInfo) indexDownloadInfo->setVisible(set); if (indexDownloadCancel) indexDownloadCancel->setVisible(set); if (indexDownloadPerc) indexDownloadPerc->setVisible(set); - checkBox->setEnabled(!set); } void OpsAstrometryIndexFiles::disconnectDownload(QMetaObject::Connection *cancelConnection, QMetaObject::Connection *replyConnection, QMetaObject::Connection *percentConnection) { if(cancelConnection) disconnect(*cancelConnection); if(replyConnection) disconnect(*replyConnection); if(percentConnection) disconnect(*percentConnection); } void OpsAstrometryIndexFiles::downloadOrDeleteIndexFiles(bool checked) { QCheckBox *checkBox = qobject_cast(QObject::sender()); - QString astrometryDataDir; - if (getAstrometryDataDir(astrometryDataDir) == false) + if (indexLocations->count() == 0) return; + QString astrometryDataDir = indexLocations->currentText(); + if(!QFileInfo(astrometryDataDir).exists()) + { + KSNotification::sorry(i18n("The selected Index File directory does not exist. Please either create it or choose another.")); + } + if (checkBox) { QString indexSeriesName = checkBox->text().remove('&'); -#ifdef Q_OS_LINUX - QString filePath = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Literal("astrometry") + '/' + indexSeriesName; -#else QString filePath = astrometryDataDir + '/' + indexSeriesName; -#endif QString fileNumString = indexSeriesName.mid(8, 2); int indexFileNum = fileNumString.toInt(); if (checked) { + if(checkBox->styleSheet() != "") //This means that the checkbox has a stylesheet so the index file was installed someplace. + { + if (KMessageBox::Cancel == KMessageBox::warningContinueCancel( + nullptr, i18n("The file %1 already exists in another directory. Are you sure you want to download it to this directory as well?", indexSeriesName), + i18n("Install File(s)"), KStandardGuiItem::cont(), + KStandardGuiItem::cancel(), "install_index_files_warning")) + { + slotUpdate(); + return; + } + } checkBox->setChecked(!checked); if (astrometryIndicesAreAvailable()) { QString URL; if (indexSeriesName.startsWith(QLatin1String("index-41"))) URL = "http://broiler.astrometry.net/~dstn/4100/" + indexSeriesName; else if (indexSeriesName.startsWith(QLatin1String("index-42"))) URL = "http://broiler.astrometry.net/~dstn/4200/" + indexSeriesName; int maxIndex = 0; if (indexFileNum < 8 && URL.contains("*")) { maxIndex = 11; if (indexFileNum < 5) maxIndex = 47; } double fileSize = 1E11 * qPow(astrometryIndex.key(fileNumString), -1.909); //This estimates the file size based on skymark size obtained from the index number. if(maxIndex != 0) fileSize /= maxIndex; //FileSize is divided between multiple files for some index series. downloadIndexFile(URL, filePath, checkBox, 0, maxIndex, fileSize); } else { KSNotification::sorry(i18n("Could not contact Astrometry Index Server: broiler.astrometry.net")); } } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel( nullptr, i18n("Are you sure you want to delete these index files? %1", indexSeriesName), i18n("Delete File(s)"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "delete_index_files_warning")) { - bool filesDeleted = false; - // Try to delete local files first + if (QFileInfo(astrometryDataDir).isWritable()) { QStringList nameFilter("*.fits"); QDir directory(astrometryDataDir); QStringList indexList = directory.entryList(nameFilter); for (auto &fileName : indexList) { if (fileName.contains(indexSeriesName.left(10))) { if (!directory.remove(fileName)) { KSNotification::error(i18n("File Delete Error")); slotUpdate(); return; } - - filesDeleted = true; + slotUpdate(); } } } - - if (filesDeleted) + else { - if (QFileInfo(astrometryDataDir).isWritable()) - { - QStringList nameFilter("*.fits"); - QDir directory(astrometryDataDir); - QStringList indexList = directory.entryList(nameFilter); - for (auto &fileName : indexList) - { - if (fileName.contains(indexSeriesName.left(10))) - { - if (!directory.remove(fileName)) - { - KSNotification::error(i18n("File Delete Error")); - slotUpdate(); - return; - } - } - } - } - else - { - KSNotification::error(i18n("Astrometry Folder Permissions Error")); - slotUpdate(); - } + KSNotification::error(i18n("Astrometry Folder Permissions Error")); + slotUpdate(); } } } } } } diff --git a/kstars/ekos/align/opsastrometryindexfiles.h b/kstars/ekos/align/opsastrometryindexfiles.h index 8d9751e73..935f096c9 100644 --- a/kstars/ekos/align/opsastrometryindexfiles.h +++ b/kstars/ekos/align/opsastrometryindexfiles.h @@ -1,54 +1,53 @@ #pragma once #include "ui_opsastrometryindexfiles.h" #include #include #include #include #include class QNetworkAccessManager; class Align; class KConfigDialog; namespace Ekos { class Align; class OpsAstrometryIndexFiles : public QDialog, public Ui::OpsAstrometryIndexFiles { Q_OBJECT public: explicit OpsAstrometryIndexFiles(Align *parent); virtual ~OpsAstrometryIndexFiles() override = default; protected: void showEvent(QShowEvent *) override; public slots: void slotUpdate(); void slotOpenIndexFileDirectory(); void downloadOrDeleteIndexFiles(bool checked); private: - bool getAstrometryDataDir(QString &dataDir); void downloadIndexFile(const QString &URL, const QString &fileN, QCheckBox *checkBox, int currentIndex, int maxIndex, double fileSize); bool astrometryIndicesAreAvailable(); void setDownloadInfoVisible(QString indexSeriesName,QCheckBox *checkBox, bool set); bool fileCountMatches(QDir directory, QString indexName); void disconnectDownload(QMetaObject::Connection *cancelConnection, QMetaObject::Connection *replyConnection, QMetaObject::Connection *percentConnection); KConfigDialog *m_ConfigDialog { nullptr }; Align *alignModule { nullptr }; QNetworkAccessManager *manager { nullptr }; QMap astrometryIndex; QTimer timeoutTimer; int downloadSpeed { 0 }; //bytes per millisecond int actualdownloadSpeed { 0 }; //bytes per millisecond }; } diff --git a/kstars/ekos/align/opsastrometryindexfiles.ui b/kstars/ekos/align/opsastrometryindexfiles.ui index f20fdf9b2..4fa973bfb 100644 --- a/kstars/ekos/align/opsastrometryindexfiles.ui +++ b/kstars/ekos/align/opsastrometryindexfiles.ui @@ -1,3824 +1,3821 @@ OpsAstrometryIndexFiles 0 0 768 - 852 + 888 Dialog li { background: url(:/icons/astrometry-required.svg) no-repeat left top; } <html><head/><body><p><span style=" font-weight:600;">Offline</span> astrometry.net solver requires index files in order to solve an image. Please see the Astrometrty.net <a href="http://astrometry.net/doc/readme.html"><span style=" text-decoration: underline; color:#0000ff;">README</span></a> for details. The following list provides a complete list of the index files, along with recommended index files to install given the current CCD Field of View. Installed index files are checked. Next to each index file is an icon that represents the following:</p></body></html> true true false 32 32 32 32 <html><head/><body><p>This index file is required and must be installed for the solver to work correctly.</p></body></html> :/icons/astrometry-required.svg true <html><head/><body><p>This index file is required and must be installed for the solver to work correctly.</p></body></html> Required Qt::Horizontal 13 20 false 32 32 32 32 <html><head/><body><p>This index file is recommended. Installing the index file might help in improving the solver.</p></body></html> :/icons/astrometry-recommended.svg true <html><head/><body><p>This index file is recommended. Installing the index file might help in improving the solver.</p></body></html> Recommended Qt::Horizontal 13 20 false 32 32 32 32 <html><head/><body><p>This index file is not required.</p></body></html> :/icons/astrometry-optional.svg true <html><head/><body><p>This index file is not required.</p></body></html> Optional - - + + - <html><head/><body><p>This displays the path to the folder for the Astrometry Index Files on your computer.</p></body></html> + <html><head/><body><p>This button will open the Astrometry Index File folder on your filesystem so that you can see where it is located and copy files into it if needed.</p></body></html> - Index Files Location: + Open - - - false - + + + + - <html><head/><body><p>This displays the path to the folder for the Astrometry Index Files on your computer.</p></body></html> + <html><head/><body><p>This displays the current CCD field of view that will be used to calculate which index files are needed.</p></body></html> + + + Current CCD FOV: - - + + - <html><head/><body><p>This button will open the Astrometry Index File folder on your filesystem so that you can see where it is located and copy files into it if needed.</p></body></html> + <html><head/><body><p>This displays the path to the folder for the Astrometry Index Files on your computer.</p></body></html> - Open + Index Files Location: - + <html><head/><body><p>This displays the current CCD field of view that will be used to calculate which index files are needed.</p></body></html> - Current CCD FOV: + Folder Details: + + + + + + + false - + true - - - - Qt::Horizontal - - - - 40 - 20 - - - - Index Files index-4210.fits index-4208.fits (arcminutes) SkyMark 0 0 0 0 75 20 0 100 0 0 0 0 0 75 20 0 100 0 (242 K) 680' - 1000' 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X Wide Fields Qt::AlignCenter (160 K) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4115.fits index-4111.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (2.1 M) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4107.fits 0 5.6' - 8.0' 0 0 0 0 75 20 0 100 0 info (129 K) (208 K) perc index-4201-*.fits index-4207-*.fits (1.3 M) 0 4.0' - 5.6' 0 0 0 0 75 20 0 100 0 info (20 M) (4.8 G) 480' - 680' (723 K) perc 0 16' - 22' 0 0 0 0 75 20 0 100 0 info (9.7 M) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 120' - 170' 0 0 0 0 75 20 0 100 0 perc 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X perc Fields Qt::AlignCenter index-4218.fits (1.2 G) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (39 M) perc index-4119.fits index-4202-*.fits Tycho2 Catalog Qt::AlignCenter (24 M) (78 M) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X perc index-4117.fits 0 8' - 11' 0 0 0 0 75 20 0 100 0 info index-4212.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4116.fits (2.6 M) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (141 K) Narrow - Medium Qt::AlignCenter index-4203-*.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4108.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4217.fits 1400' - 2000' 170' - 240' (624 M) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4214.fits 240' - 340' (312 M) (filesize) Qt::AlignCenter 340' - 480' 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (183 K) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 42' - 60' 0 0 0 0 75 20 0 100 0 perc 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (4 M) index-4206-*.fits (8.8 G) index-4109.fits index-4113.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 2.8' - 4.0' 0 0 0 0 75 20 0 100 0 info (156 M) index-4205-*.fits 0 0 0 0 75 20 0 100 0 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4118.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4213.fits 0 2.0' - 2.8' 0 0 0 0 75 20 0 100 0 info index-4219.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4204-*.fits 0 0 0 0 75 20 0 100 0 2Mass Catalog Qt::AlignCenter Diameters 0 11' - 16' 0 0 0 0 75 20 0 100 0 info index-4215.fits 0 0 0 0 75 20 0 100 0 0 0 0 0 75 20 0 100 0 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (13.6 G) index-4200-*.fits 0 60' - 85' 0 0 0 0 75 20 0 100 0 perc index-4110.fits (1 M) index-4114.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X perc (157 M) (filesize) 0 22' - 30' 0 0 0 0 75 20 0 100 0 info (399 K) (582 K) 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X (90 M) index-4216.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X 0 30' - 42' 0 0 0 0 75 20 0 100 0 perc index-4211.fits 0 0 15 15 15 15 255 0 0 255 0 0 148 148 148 X index-4209.fits (332 K) (2.5 G) index-4112.fits (7.6 M) (5.1 M) (47 M) 0 85' - 120' 0 0 0 0 75 20 0 100 0 perc 1000' - 1400' diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg index c33f89709..cbd5e5476 100644 --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -1,2436 +1,2431 @@ ksutils.h The screen coordinates of the Time InfoBox. QPoint(0,0) The screen coordinates of the Focus InfoBox. QPoint(600,0) The screen coordinates of the Geographic Location InfoBox. QPoint(0,600) If true, the Time InfoBox will show only its top line of data. true If true, the Focus InfoBox will show only its top line of data. true If true, the Geographic Location InfoBox will show only its top line of data. true Toggles display of all three InfoBoxes. true Toggles display of the Time InfoBox. true Toggles display of the Focus InfoBox. true Toggles display of the Geographic Location InfoBox. true Is the Time InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 0 0 3 Is the Focus InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 1 0 3 Is the Geographic Location InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 2 0 3 Toggle display of the status bar. true Toggle display of the Horizontal coordinates of the mouse cursor in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the current epoch in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the standard epoch in the status bar. false true 1024 768 true Black Body List of the filenames of custom object catalogs. List of integers toggling display of each custom object catalog (any nonzero value indicates the objects in that catalog will be displayed). List of names for which custom catalogs are to be displayed. Names of objects entered into the find dialog are resolved using online services and stored in the database. This option also toggles the display of such resolved objects on the sky map. true 800 600 true true false Toggle display of crosshairs centered at telescope's pointed position in the KStars sky map. true Toggle display of INDI messages in the KStars statusbar. true Show INDI messages as desktop notifications instead of dialogs. false true false false The default location of saved FITS files KSUtils::getDefaultPath("fitsDir") INDI server will attempt to bind with ports starting from this port 7624 INDI server will attempt to bind with ports ending with this port 9000 List of the aliases for filter wheel slots. PATH to indiserver binary KSUtils::getDefaultPath("indiServer") false PATH to indi drivers directory KSUtils::getDefaultPath("indiDriversDir") false 320 240 false false false false false false false false false false false The City name of the current geographic location. Greenwich The Province name of the current geographic location. This is the name of the state for locations in the U. S. The Country name of the current geographic location. United Kingdom The longitude of the current geographic location, in decimal degrees. 0.0 The latitude of the current geographic location, in decimal degrees. 51.468 -10.0 0.0 Two-letter code that determines the dates on which daylight savings time begins and ends (you can view the rules by pressing the "Explain DST Rules" button in the Geographic Location window). -- If true, focus changes will cause the sky to visibly spin to the new position. Otherwise, the display will "snap" instantly to the new position. true If true, clicking on the skymap will select the closest object and highlights it. false Type of cursor when exploring the sky map. 1 The names of the currently selected field-of-view indicators. The list of defined FOV indicator names is listed in the "Settings|FOV Symbols" menu. Telrad If true, trails attached to solar system bodies will fade into the background sky color. true The right ascension of the initial focus position of the sky map, in decimal hours. This value is volatile; it is reset whenever the program shuts down. 180.0 The declination of the initial focus position of the sky map, in decimal degrees. This value is volatile; it is reset whenever the program shuts down. 45.0 The name of the object that should be centered and tracked on startup. If no object should be centered, set to "nothing". This value is volatile; it is reset whenever the program shuts down. nothing True if the skymap should track on its initial position on startup. This value is volatile; it is reset whenever the program shuts down. false Toggle whether KStars should hide some objects while the display is moving, for smoother motion. true Toggle whether constellation boundaries are hidden while the display is in motion. true Toggle whether constellation lines are hidden while the display is in motion. false Choose sky culture. 11 Toggle whether constellation names are hidden while the display is in motion. false Toggle whether the coordinate grids are hidden while the display is in motion. true Toggle whether the Milky Way contour is hidden while the display is in motion. true Toggle whether IC objects are hidden while the display is in motion. true Toggle whether Messier objects are hidden while the display is in motion. true Toggle whether NGC objects are hidden while the display is in motion. true Toggle whether extra objects are hidden while the display is in motion. true Toggle whether solar system objects are hidden while the display is in motion. false Toggle whether faint stars are hidden while the display is in motion. true Toggle whether name labels are hidden while the display is in motion. true Toggle whether asteroids are drawn in the sky map. true Toggle whether asteroid name labels are drawn in the sky map. false true Toggle whether comets are drawn in the sky map. true Toggle whether comet comas are drawn in the sky map. true Toggle whether comet name labels are drawn in the sky map. false Toggle whether supernovae are drawn in the sky map. false Toggle whether supernova name labels are drawn in the sky map. false Set magnitude limit for supernovae to be shown on the skymap. 16 Toggle supernova alerts. true Set magnitude limit for supernovae to be alerted. 13 Toggle whether constellation boundaries are drawn in the sky map. false Toggle whether constellation boundary containing the central focus point is highlighted in the sky map. false Toggle whether constellation lines are drawn in the sky map. false Toggle whether constellation art drawn in the sky map. false Toggle whether constellation name labels are drawn in the sky map. false Toggle whether deep-sky objects are drawn in the sky map. true Toggle whether the ecliptic line is drawn in the sky map. false Toggle whether the equator line is drawn in the sky map. false Coordinate grids will automatically change according to active coordinate system. true Toggle whether the equatorial coordinate grid is drawn in the sky map. false Toggle whether the horizontal coordinate grid is drawn in the sky map. false Toggle whether the local meridian line is drawn in the sky map. false Toggle whether the region below the horizon is opaque. true Toggle whether the horizon line is drawn in the sky map. true Toggle whether flags are drawn in the sky map. true Toggle whether IC objects are drawn in the sky map. false Toggle whether NGC objects are drawn in the sky map. true Toggle whether Messier objects are drawn in the sky map. true Toggle whether Messier objects are rendered as images in the sky map. true Toggle whether extra objects are drawn in the sky map. true Toggle whether the Milky Way contour is drawn in the sky map. true Toggle whether the Milky Way contour is filled. When this option is false, the Milky Way is shown as an outline. true Meta-option to control whether all major planets (and the Sun and Moon) are drawn in the sky map. true Toggle whether major planets (and the Sun and Moon) are rendered as images in the sky map. true Toggle whether major planets (and the Sun and Moon) are labeled in the sky map. true Toggle whether the Sun is drawn in the sky map. true Toggle whether the Moon is drawn in the sky map. true Toggle whether Mercury is drawn in the sky map. true Toggle whether Venus is drawn in the sky map. true Toggle whether Mars is drawn in the sky map. true Toggle whether Jupiter is drawn in the sky map. true Toggle whether Saturn is drawn in the sky map. true Toggle whether Uranus is drawn in the sky map. true Toggle whether Neptune is drawn in the sky map. true Toggle whether Pluto is drawn in the sky map. true Toggle whether stars are drawn in the sky map. true Toggle whether star magnitude (brightness) labels are shown in the sky map. false Toggle whether star name labels are shown in the sky map. true Toggle whether deep-sky object magnitude (brightness) labels are shown in the sky map. false Toggle whether deep-sky object name labels are shown in the sky map. false The timescale above which slewing mode is forced on at all times. 60 The background fill mode for the on-screen information boxes: 0="no BG"; 1="semi-transparent BG"; 2="opaque BG" 1 Algorithm for the mapping projection. 0 Use official IAU abbreviations for constellation names. false Use Latin constellation names. false Use localized constellation names (if localized names are not available, default to Latin names). true Display the sky with horizontal coordinates (when false, equatorial coordinates will be used). true Toggle whether a centered object automatically gets a name label attached. true Toggle whether a centered solar system object automatically gets a trail attached, as long as it remains centered. true Toggle whether the object under the mouse cursor gets a transient name label. true Toggle whether object positions are corrected for the effects of atmospheric refraction (only applies when horizontal coordinates are used). true Toggle whether corrections due to bending of light around the sun are taken into account false Toggle whether the sky is rendered using antialiasing. Lines and shapes are smoother with antialiasing, but rendering the screen will take more time. true The zoom level, measured in pixels per radian. 250. 250. 5000000. When zooming in or out, change zoom speed factor by this multiplier. 0.2 0.01 1.0 The faint magnitude limit for drawing asteroids. 15.0 The maximum magnitude (visibility) to filter the asteroid data download from JPL. 12.000 Controls the relative number of asteroid name labels drawn in the map. 4.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed in. 16.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed out. 5.0 When enabled, objects whose magnitudes are unknown, or not available to KStars, are drawn irrespective of the faint limits set. true Sets the density of stars in the field of view 5 The faint magnitude limit for drawing stars, when the map is in motion (only applicable if faint stars are set to be hidden while the map is in motion). 5.0 The relative density for drawing star name and magnitude labels. 2.0 The relative density for drawing deep-sky object name and magnitude labels. 5.0 If true, long names (common names) for deep-sky objects are shown in the labels. false The maximum solar distance for drawing comets. 3.0 Use experimental OpenGL backend (deprecated). false The state of the clock (running or not) true Objects in the observing list will be highlighted with a symbol in the map. true Objects in the observing list will be highlighted with a colored name label in the map. false The observing list will prefer DSS imagery while downloading imagery. true The observing list will prefer SDSS imagery while downloading imagery. false Check this if you use a large Dobsonian telescope. Sorting by percentage current altitude is an easy way of determining what objects are well-placed for observation. However, when using a large Dobsonian telescope, objects close to the zenith are hard to observe. Since tracking there corresponds to a rotation in azimuth, it is both counterintuitive and requires the observer to frequently move the ladder. The region around the zenith where this is particularly frustrating is called the Dobsonian hole. This checkbox makes the observing list consider objects present in the hole as unfit for observation. false This specifies the angular radius of the Dobsonian hole, i.e. the region where a large Dobsonian telescope cannot be pointed easily. 15.00 40.00 The name of the color scheme moonless-night.colors The method for rendering stars: 0="realistic colors"; 1="solid red"; 2="solid black"; 3="solid white"; 4="solid real colors" 0 4 The color saturation level of stars (only applicable when using "realistic colors" mode). 6 10 The color for the angular-distance measurement ruler. #FFF The background color of the on-screen information boxes. #000 The text color for the on-screen information boxes, when activated by a mouse click. #F00 The normal text color of the on-screen information boxes. #FFF The color for the constellation boundary lines. #222 The color for the constellation boundary lines. #222 The color for the constellation figure lines. #555 The color for the constellation names. #AA7 The color for the cardinal compass point labels. #002 The color for the ecliptic line. #663 The color for the equator line. #FFF The color for the equatorial coordinate grid lines. #456 The color for the horizontal coordinate grid lines. #5A3 The color for objects which have extra URL links available. #A00 The color for the horizon line and opaque ground. #5A3 The color for the local meridian line. #0059b3 The color for Messier object symbols. #0F0 The color for NGC object symbols. #066 The color for IC object symbols. #439 The color for the Milky Way contour. #123 The color for star name labels. #7AA The color for deep-sky object name labels. #7AA The color for solar system object labels. #439 The color for solar system object trails. #963 The color for the sky background. #002 The color for the artificial horizon region. #C82828 The color for telescope target symbols. #8B8 Color of visible satellites. #00FF00 Color of invisible satellites. #FF0000 Color of satellites labels. #640000 Color of supernova #FFA500 The color for user-added object labels. #439 The color for RA Guide Error bar in Ekos guide module. #00FF00 The color for DEC Guide Error bar in Ekos guide module. #00A5FF The color for solver FOV box in Ekos alignment module. #FFFF00 false Xplanet binary path KSUtils::getDefaultPath("XplanetPath") Option to use a FIFO file instead of saving to the hard disk true How long to wait for XPlanet before giving up in milliseconds 1000 How long to pause between frames in the XPlanet Animation 100 Width of xplanet window 640 Height of xplanet window 480 If true, display a label in the upper right corner. false Show local time. true Show GMT instead of local time. false Specify the text of the first line of the label. By default, it says something like "Looking at Earth". Any instances of %t will be replaced by the target name, and any instances of %o will be replaced by the origin name. Specify the point size. 12 Set the color for the label. #F00 Specify the format for the date/time label. This format string is passed to strftime(3). The default is "%c %Z", which shows the date, time, and time zone in the locale’s appropriate date and time representation. %c %Z false true false false Draw a glare around the sun with a radius of the specified value larger than the Sun. The default value is 28. 28 Place the observer above a random latitude and longitude false Place the observer above the specified longitude and latitude true Render the target body as seen from above the specified latitude (in degrees). The default value is 0. 0 Place the observer above the specified longitude (in degrees). Longitude is positive going east, negative going west (for the earth and moon), so for example Los Angeles is at -118 or 242. The default value is 0. 0 The default is no projection. Multiple bodies will not be shown if this option is specified, although shadows will still be drawn. 0 Use a file as the background image, with the planet to be superimposed upon it. This option is only meaningful with the -projection option. A color may also be supplied. false Use a file as the background image. false The path of the background image. Use a color as the background. true The color of the background. #000 A star of the specified magnitude will have a pixel brightness of 1. The default value is 10. Stars will be drawn more brightly if this number is larger. 10 If checked, use an arc file to be plotted against the background stars. false Specify an arc file to be plotted against the background stars. If checked, use a config file. false Use the specified configuration file. If checked, use kstars's FOV. false If checked, use the specified marker file. false Specify a file containing user-defined marker data to display against the background stars. If checked, write coordinates of the bounding box for each marker in a file. false Write coordinates of the bounding box for each marker to this file. If checked, use star map file to draw the background stars. false Star map file path This option is only used when creating JPEG images. The quality can range from 0 to 100. The default value is 80. 80 Toggle whether satellite tracks are drawn in the sky map. false Toggle whether satellite tracks are drawn in the sky map. false If selected, satellites will be draw like stars, otherwise, draw satellites as small colored square. false Toggle whether satellite labels are drawn in the sky map. false List of selected satellites. Checking this option causes recomputation of current equatorial coordinates from catalog coordinates (i.e. application of precession, nutation and aberration corrections) for every redraw of the map. This makes processing slower when there are many stars to handle, but is more likely to be bug free. There are known bugs in the rendering of stars when this recomputation is avoided. false The default size for DSS images downloaded from the Internet. 15.0 To include parts of the star field, we add some extra padding around DSS images of deep-sky objects. This option configures the total (both sides) padding added to either dimension of the field. 10.0 Checking this option causes KStars to generate verbose debug information for diagnostic purposes. This may cause slowdown of KStars. false Checking this option causes KStars to generate regular debug information. true Checking this option causes KStars to stop generating ANY debug information. false Checking this option causes KStars log debug messages to the default output used by the platform (e.g. Standard Error). true Checking this option causes KStars log debug messages to a log file as specified. false Log FITS Data activity. false Log INDI devices activity. false Log Ekos Capture Module activity. false Log Ekos Focus Module activity. false Log Ekos Guide Module activity. false Log Ekos Alignment Module activity. false Log Ekos Mount Module activity. false Log Ekos Observatory Module activity. false true Display all captured FITS images in a single tab instead of multiple tabs per image. true Display all captured FITS images in a single FITS Viewer window. By default each camera create its own FITS Viewer instance false Display all opened FITS images in a single FITS Viewer window. true Bring the FITSViewer window to the foreground when receiving a new image. true false !KSUtils::isHardwareLimited() false !KSUtils::isHardwareLimited() !KSUtils::isHardwareLimited() KSUtils::isHardwareLimited() 4 false false 40.0 0 600 600 true false true true true false Simulators false true false true 1 Minimum telescope altitude limit. If the telescope is below this limit, it will be commanded to stop. 0 Maximum telescope altitude limit. If the telescope is above this limit, it will be commanded to stop. 90.0 false If the target hour angle exceeds this value, Ekos will command a meridian flip and if successful it will resume guiding and capture operations. false true false false false 3:00 AM false 1 0 If guide deviation exceeds this limit, the exposure will be automatically aborted and only resumed when the deviation is within this limit. 2 If HFR deviation exceeds this limit, the autofocus routine will be automatically started. 0.5 false false false Sets the time interval before forced autofocus attempts during a capture sequence. 60 false If set, Ekos will capture a few flat images to determine the optimal exposure time to achieve the desired ADU value. 0 Maximum difference between measured and target ADU values to deem the value as acceptable. 1000 0 0 0 0 0.1 0 false false 2.5 true false 1 30 true !KSUtils::isHardwareLimited() !KSUtils::isHardwareLimited() 0 KSUtils::getDefaultPath("fitsDir") 60 false false false Step size of the absolute focuser. The step size TICKS should be adjusted so that when the focuser moves TICKS steps, the difference in HFR is more than 0.1 pixels. Lower the value when you are close to optimal focus. 100 Wait for this many seconds after moving the focuser before capturing the next image during AutoFocus. 0 Wait for this many seconds after resuming guide. 0 The tolerance specifies the percentage difference between the current focusing position and the minimum obtained during the focusing run. Adjustment of this value is necessary to prevent the focusing algorithm from oscillating back and forth. 1 Set the maximum travel distance of an absolute focuser. 10000 Specifies gain value of CCD when performing focusing if supported by camera. 0 Set box size to select a focus star. 64 Set horizontal binning of CCD camera while in focus mode. 1 Set vertical binning of CCD camera while in focus mode. 1 true false During full field focusing, stars which are inside this percentage of the frame are filtered out of HFR calculation (default 0%). Detection algorithms may also have an inherent filter. 0.0 During full field focusing, stars which are outside this percentage of the frame are filtered out of HFR calculation (default 100%). Detection algorithms may also have an inherent filter. 100.0 false true false 0 150 0 0 1 Specifies exposure value of CCD in seconds when performing plate solving. 1 Set binning index of CCD camera while in alignment mode. Default values 0-3 corresponding to 1x1 to 4x4 binning. 4 is max binning. 4 Use rotator when performing load and slew. false Threshold between measured and FITS position angles in arcminutes to consider the load and slew operation successful. 30 0 0 1 true false false 30 0 false 1500 false true true true true 1 true 2 true true true 30 false Path to astrometry.net solver location. KSUtils::getDefaultPath("AstrometrySolverBinary") false Path to astrometry.net wcsinfo location. KSUtils::getDefaultPath("AstrometryWCSInfo") false Path to astrometry.net file location. KSUtils::getDefaultPath("AstrometryConfFile") - - - Astrometry.net Index files Location. - KSUtils::getDefaultPath("AstrometryIndexFileLocation") - - false + true true Folder in which the desired python executable or link to be used for astrometry.net resides. /usr/local/opt/python/libexec/bin Key to access astrometry.net online web services. You must register with astrometry.net to obtain a key. iczikaqstszeptgs http://nova.astrometry.net true 180 -1 true 1.0 0 0 localhost 4400 localhost 5656 0 1000 2 false 1 false false false 3 60 10 true false false false 2 1 0 1 45 10 500 false false false 2 true true true true true true 133.33 133.33 0 0 0 0 5000 5000 100 100 0.5 2 true true false false Log Ekos Scheduler Module activity. false Sort scheduler jobs by priority and altitude. true true false false false false true false 2 true 5 30 3 0 0 0 0 0 0 1 0 false 7624 8624 300 1000 None false false Toggle whether the HIPS sources are drawn in the sky map. false true true false false true 600 true true true 30 true true true