1 /*  ExternalSextractorSolver, StellarSolver Internal Library developed by Robert Lancaster, 2020
2 
3     This application is free software; you can redistribute it and/or
4     modify it under the terms of the GNU General Public
5     License as published by the Free Software Foundation; either
6     version 2 of the License, or (at your option) any later version.
7 */
8 #include "externalsextractorsolver.h"
9 #include <QTextStream>
10 #include <QMessageBox>
11 #include <qmath.h>
12 #include <wcshdr.h>
13 #include <wcsfix.h>
14 
15 static int solverNum = 1;
16 
ExternalSextractorSolver(ProcessType type,ExtractorType sexType,SolverType solType,FITSImage::Statistic imagestats,uint8_t const * imageBuffer,QObject * parent)17 ExternalSextractorSolver::ExternalSextractorSolver(ProcessType type, ExtractorType sexType, SolverType solType,
18         FITSImage::Statistic imagestats, uint8_t const *imageBuffer, QObject *parent) : InternalSextractorSolver(type, sexType,
19                     solType, imagestats, imageBuffer, parent)
20 {
21 
22     //This sets the base name used for the temp files.
23     m_BaseName = "externalSextractorSolver_" + QString::number(solverNum++);
24 
25     //The code below sets default paths for these key external file settings.
26 
27 #if defined(Q_OS_OSX)
28     if(QFile("/usr/local/bin/solve-field").exists())
29         setExternalFilePaths(getMacHomebrewPaths());
30     else
31         setExternalFilePaths(getMacInternalPaths());
32 #elif defined(Q_OS_LINUX)
33     setExternalFilePaths(getLinuxDefaultPaths());
34 #else //Windows
35     setExternalFilePaths(getWinANSVRPaths());
36 #endif
37 
38 }
39 
~ExternalSextractorSolver()40 ExternalSextractorSolver::~ExternalSextractorSolver()
41 {
42     delete xcol;
43     delete ycol;
44     delete magcol;
45     delete colFormat;
46     delete colUnits;
47     delete magUnits;
48 }
49 
50 //The following methods are available to get the default paths for different operating systems and configurations.
51 
getLinuxDefaultPaths()52 ExternalProgramPaths ExternalSextractorSolver::getLinuxDefaultPaths()
53 {
54     return ExternalProgramPaths
55     {
56         "/etc/astrometry.cfg",          //confPath
57         "/usr/bin/sextractor",          //sextractorBinaryPath
58         "/usr/bin/solve-field",         //solverPath
59         (QFile("/bin/astap").exists()) ?
60         "/bin/astap" :              //astapBinaryPath
61         "/opt/astap/astap",
62         "/usr/bin/wcsinfo"              //wcsPath
63     };
64 }
65 
getLinuxInternalPaths()66 ExternalProgramPaths ExternalSextractorSolver::getLinuxInternalPaths()
67 {
68     return ExternalProgramPaths
69     {
70         "$HOME/.local/share/kstars/astrometry/astrometry.cfg",  //confPath
71         "/usr/bin/sextractor",                                  //sextractorBinaryPath
72         "/usr/bin/solve-field",                                 //solverPath
73         (QFile("/bin/astap").exists()) ?
74         "/bin/astap" :                                      //astapBinaryPath
75         "/opt/astap/astap",
76         "/usr/bin/wcsinfo"                                      //wcsPath
77     };
78 }
79 
getMacHomebrewPaths()80 ExternalProgramPaths ExternalSextractorSolver::getMacHomebrewPaths()
81 {
82     return ExternalProgramPaths
83     {
84         "/usr/local/etc/astrometry.cfg",                //confPath
85         "/usr/local/bin/sex",                           //sextractorBinaryPath
86         "/usr/local/bin/solve-field",                   //solverPath
87         "/Applications/ASTAP.app/Contents/MacOS/astap", //astapBinaryPath
88         "/usr/local/bin/wcsinfo"                        //wcsPath
89     };
90 }
91 
getMacInternalPaths()92 ExternalProgramPaths ExternalSextractorSolver::getMacInternalPaths()
93 {
94     return ExternalProgramPaths
95     {
96         "/Applications/KStars.app/Contents/MacOS/astrometry/bin/astrometry.cfg",    //confPath
97         "/Applications/KStars.app/Contents/MacOS/astrometry/bin/sex",               //sextractorBinaryPath
98         "/Applications/KStars.app/Contents/MacOS/astrometry/bin/solve-field",       //solverPath
99         "/Applications/ASTAP.app/Contents/MacOS/astap",                             //astapBinaryPath
100         "/Applications/KStars.app/Contents/MacOS/astrometry/bin/wcsinfo"            //wcsPath
101     };
102 }
103 
getWinANSVRPaths()104 ExternalProgramPaths ExternalSextractorSolver::getWinANSVRPaths()
105 {
106     return ExternalProgramPaths
107     {
108         QDir::homePath() + "/AppData/Local/cygwin_ansvr/etc/astrometry/backend.cfg",    //confPath
109         "",                                                                             //sextractorBinaryPath
110         QDir::homePath() + "/AppData/Local/cygwin_ansvr/lib/astrometry/bin/solve-field.exe",               //solverPath
111         "C:/Program Files/astap/astap.exe",                                             //astapBinaryPath
112         QDir::homePath() + "/AppData/Local/cygwin_ansvr/lib/astrometry/bin/wcsinfo.exe" //wcsPath
113     };
114 }
115 
getWinCygwinPaths()116 ExternalProgramPaths ExternalSextractorSolver::getWinCygwinPaths()
117 {
118     return ExternalProgramPaths
119     {
120         "C:/cygwin64/usr/etc/astrometry.cfg",   //confPath
121         "",                                     //sextractorBinaryPath
122         "C:/cygwin64/bin/solve-field",          //solverPath
123         "C:/Program Files/astap/astap.exe",     //astapBinaryPath
124         "C:/cygwin64/bin/wcsinfo"               //wcsPath
125     };
126 }
127 
setExternalFilePaths(ExternalProgramPaths paths)128 void ExternalSextractorSolver::setExternalFilePaths(ExternalProgramPaths paths)
129 {
130     confPath = paths.confPath;
131     sextractorBinaryPath = paths.sextractorBinaryPath;
132     solverPath = paths.solverPath;
133     astapBinaryPath = paths.astapBinaryPath;
134     wcsPath = paths.wcsPath;
135 }
136 
extract()137 int ExternalSextractorSolver::extract()
138 {
139     if(m_ExtractorType == EXTRACTOR_EXTERNAL)
140     {
141 #ifdef _WIN32  //Note that this is just a warning, if the user has Sextractor installed somehow on Windows, they could use it.
142         emit logOutput("Sextractor is not easily installed on windows. Please select the Internal Sextractor and External Solver.");
143 #endif
144 
145         if(!QFileInfo(sextractorBinaryPath).exists())
146         {
147             emit logOutput("There is no sextractor at " + sextractorBinaryPath + ", Aborting");
148             return -1;
149         }
150     }
151 
152     if(sextractorFilePath == "")
153     {
154         sextractorFilePathIsTempFile = true;
155         sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
156     }
157 
158     if(m_ProcessType == EXTRACT_WITH_HFR || m_ProcessType == EXTRACT)
159         return runExternalSextractor();
160     else
161     {
162         int fail = 0;
163         if(m_ExtractorType == EXTRACTOR_INTERNAL)
164         {
165             fail = runSEPSextractor();
166             if(fail != 0)
167                 return fail;
168             return(writeSextractorTable());
169         }
170         else if(m_ExtractorType == EXTRACTOR_EXTERNAL)
171             return(runExternalSextractor());
172     }
173     return -1;
174 }
175 
run()176 void ExternalSextractorSolver::run()
177 {
178     if(computingWCS)
179     {
180         if(m_HasSolved)
181         {
182             computeWCSCoord();
183             emit finished(0);
184         }
185         else
186             emit finished(-1);
187         computingWCS = false;
188         return;
189     }
190 
191     if(m_AstrometryLogLevel != LOG_NONE && m_LogToFile)
192     {
193         if(m_LogFileName == "")
194             m_LogFileName = m_BasePath + "/" + m_BaseName + ".log.txt";
195         if(QFile(m_LogFileName).exists())
196             QFile(m_LogFileName).remove();
197     }
198 
199     if(cancelfn == "")
200         cancelfn = m_BasePath + "/" + m_BaseName + ".cancel";
201     if(solvedfn == "")
202         solvedfn = m_BasePath + "/" + m_BaseName + ".solved";
203     if(solutionFile == "")
204         solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
205 
206     QFile solvedFile(solvedfn);
207     solvedFile.setPermissions(solvedFile.permissions() | QFileDevice::WriteOther);
208     solvedFile.remove();
209 
210     QFile(cancelfn).remove();
211 
212     //These are the solvers that use External Astrometry.
213     if(m_SolverType == SOLVER_LOCALASTROMETRY)
214     {
215         if(!QFileInfo(solverPath).exists())
216         {
217             emit logOutput("There is no astrometry solver at " + solverPath + ", Aborting");
218             emit finished(-1);
219             return;
220         }
221 #ifdef _WIN32
222         if(m_ActiveParameters.inParallel)
223         {
224             emit logOutput("The external ANSVR solver on windows does not handle the inparallel option well, disabling it for this run.");
225             m_ActiveParameters.inParallel = false;
226         }
227 #endif
228     }
229     else if(m_SolverType == SOLVER_ASTAP)
230     {
231         if(!QFileInfo(astapBinaryPath).exists())
232         {
233             emit logOutput("There is no ASTAP solver at " + astapBinaryPath + ", Aborting");
234             emit finished(-1);
235             return;
236         }
237     }
238 
239     if(sextractorFilePath == "")
240     {
241         sextractorFilePathIsTempFile = true;
242         sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
243     }
244 
245     switch(m_ProcessType)
246     {
247         case EXTRACT:
248         case EXTRACT_WITH_HFR:
249         {
250             int result = extract();
251             cleanupTempFiles();
252             emit finished(result);
253         }
254         break;
255 
256         case SOLVE:
257         {
258             if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_LOCALASTROMETRY)
259             {
260                 int result = runExternalSolver();
261                 cleanupTempFiles();
262                 emit finished(result);
263             }
264             else if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_ASTAP)
265             {
266                 int result = runExternalASTAPSolver();
267                 cleanupTempFiles();
268                 emit finished(result);
269             }
270             else
271             {
272                 if(!m_HasExtracted)
273                 {
274                     int fail = extract();
275                     if(fail != 0)
276                     {
277                         cleanupTempFiles();
278                         emit finished(fail);
279                         return;
280                     }
281                     if(m_ExtractedStars.size() == 0)
282                     {
283                         cleanupTempFiles();
284                         emit logOutput("No stars were found, so the image cannot be solved");
285                         emit finished(-1);
286                         return;
287                     }
288                 }
289 
290                 if(m_HasExtracted)
291                 {
292                     if(m_SolverType == SOLVER_ASTAP)
293                     {
294                         int result = runExternalASTAPSolver();
295                         cleanupTempFiles();
296                         emit finished(result);
297                     }
298                     else
299                     {
300                         int result = runExternalSolver();
301                         cleanupTempFiles();
302                         emit finished(result);
303                     }
304                 }
305                 else
306                 {
307                     cleanupTempFiles();
308                     emit finished(-1);
309                 }
310             }
311 
312         }
313         break;
314 
315         default:
316             break;
317     }
318 }
319 
320 //This method generates child solvers with the options of the current solver
spawnChildSolver(int n)321 SextractorSolver* ExternalSextractorSolver::spawnChildSolver(int n)
322 {
323     ExternalSextractorSolver *solver = new ExternalSextractorSolver(m_ProcessType, m_ExtractorType, m_SolverType, m_Statistics,
324             m_ImageBuffer, nullptr);
325     solver->m_ExtractedStars = m_ExtractedStars;
326     solver->m_BasePath = m_BasePath;
327     solver->m_BaseName = m_BaseName + "_" + QString::number(n);
328     solver->m_HasExtracted = true;
329     solver->sextractorFilePath = sextractorFilePath;
330     solver->sextractorFilePathIsTempFile = sextractorFilePathIsTempFile;
331     solver->fileToProcess = fileToProcess;
332     solver->sextractorBinaryPath = sextractorBinaryPath;
333     solver->confPath = confPath;
334     solver->solverPath = solverPath;
335     solver->astapBinaryPath = astapBinaryPath;
336     solver->wcsPath = wcsPath;
337     solver->cleanupTemporaryFiles = cleanupTemporaryFiles;
338     solver->autoGenerateAstroConfig = autoGenerateAstroConfig;
339     solver->onlySendFITSFiles = onlySendFITSFiles;
340 
341     solver->isChildSolver = true;
342     solver->m_ActiveParameters = m_ActiveParameters;
343     solver->indexFolderPaths = indexFolderPaths;
344     //Set the log level one less than the main solver
345     if(m_SSLogLevel == LOG_VERBOSE )
346         solver->m_SSLogLevel = LOG_NORMAL;
347     if(m_SSLogLevel == LOG_NORMAL || m_SSLogLevel == LOG_OFF)
348         solver->m_SSLogLevel = LOG_OFF;
349     if(m_UseScale)
350         solver->setSearchScale(scalelo, scalehi, scaleunit);
351     if(m_UsePosition)
352         solver->setSearchPositionInDegrees(search_ra, search_dec);
353     if(m_AstrometryLogLevel != SSolver::LOG_NONE || m_SSLogLevel != SSolver::LOG_OFF)
354         connect(solver, &SextractorSolver::logOutput, this, &SextractorSolver::logOutput);
355     //This way they all share a solved and cancel fn
356     solver->solutionFile = solutionFile;
357     //solver->cancelfn = cancelfn;
358     //solver->solvedfn = basePath + "/" + baseName + ".solved";
359     return solver;
360 }
361 
362 //This is the abort method.  For the external sextractor and solver, it uses the kill method to abort the processes
abort()363 void ExternalSextractorSolver::abort()
364 {
365     QFile file(cancelfn);
366     if(QFileInfo(file).dir().exists())
367     {
368         file.open(QIODevice::WriteOnly);
369         file.write("Cancel");
370         file.close();
371     }
372     if(!isChildSolver)
373         emit logOutput("Aborting ...");
374     m_WasAborted = true;
375 }
376 
cleanupTempFiles()377 void ExternalSextractorSolver::cleanupTempFiles()
378 {
379     if(cleanupTemporaryFiles)
380     {
381         QDir temp(m_BasePath);
382         //Sextractor Files
383         temp.remove(m_BaseName + ".param");
384         temp.remove(m_BaseName + ".conv");
385         temp.remove(m_BaseName + ".cfg");
386 
387         //ASTAP files
388         temp.remove(m_BaseName + ".ini");
389 
390         //Astrometry Files
391         temp.remove(m_BaseName + ".corr");
392         temp.remove(m_BaseName + ".rdls");
393         temp.remove(m_BaseName + ".axy");
394         temp.remove(m_BaseName + ".corr");
395         temp.remove(m_BaseName + ".new");
396         temp.remove(m_BaseName + ".match");
397         temp.remove(m_BaseName + "-indx.xyls");
398         temp.remove(m_BaseName + ".solved");
399 
400         //Other Files
401         QFile solvedFile(solvedfn);
402         solvedFile.setPermissions(solvedFile.permissions() | QFileDevice::WriteOther);
403         solvedFile.remove();
404         QFile(solutionFile).remove();
405         QFile(cancelfn).remove();
406         if(sextractorFilePathIsTempFile)
407             QFile(sextractorFilePath).remove();
408         if(fileToProcessIsTempFile)
409             QFile(fileToProcess).remove();
410     }
411 }
412 
413 //This method is copied and pasted and modified from the code I wrote to use sextractor in OfflineAstrometryParser in KStars
414 //It creates key files needed to run Sextractor from the desired options, then runs the sextractor program using the options.
runExternalSextractor()415 int ExternalSextractorSolver::runExternalSextractor()
416 {
417     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
418     emit logOutput("Configuring external Sextractor");
419     QFileInfo file(fileToProcess);
420     if(!file.exists())
421         return -1;
422     if(file.suffix() != "fits" && file.suffix() != "fit")
423     {
424         int ret = saveAsFITS();
425         if(ret != 0)
426             return ret;
427     }
428     else
429     {
430         QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
431         QFile::copy(fileToProcess, newFileURL);
432         fileToProcess = newFileURL;
433         fileToProcessIsTempFile = true;
434     }
435 
436     //Configuration arguments for sextractor
437     QStringList sextractorArgs;
438     //This one is not really that necessary, it will use the defaults if it can't find it
439     //We will set all of the things we need in the parameters below
440     //sextractorArgs << "-c" << "/usr/local/share/sextractor/default.sex";
441 
442     sextractorArgs << "-CATALOG_NAME" << sextractorFilePath;
443     sextractorArgs << "-CATALOG_TYPE" << "FITS_1.0";
444 
445     //sextractor needs a default.param file in the working directory
446     //This creates that file with the options we need for astrometry.net and sextractor
447 
448     QString paramPath =  m_BasePath + "/" + m_BaseName + ".param";
449     QFile paramFile(paramPath);
450     if (paramFile.open(QIODevice::WriteOnly) == false)
451     {
452         QMessageBox::critical(nullptr, "Message", "Sextractor file write error.");
453         return -1;
454     }
455     else
456     {
457         QTextStream out(&paramFile);
458         out << "X_IMAGE\n";//                  Object position along x                                   [pixel]
459         out << "Y_IMAGE\n";//                  Object position along y                                   [pixel]
460         out << "MAG_AUTO\n";//                 Kron-like elliptical aperture magnitude                   [mag]
461         out << "FLUX_AUTO\n";//                Flux within a Kron-like elliptical aperture               [count]
462         out << "FLUX_MAX\n";//                 Peak flux above background                                [count]
463         out << "CXX_IMAGE\n";//                Cxx object ellipse parameter                              [pixel**(-2)]
464         out << "CYY_IMAGE\n";//                Cyy object ellipse parameter                              [pixel**(-2)]
465         out << "CXY_IMAGE\n";//                Cxy object ellipse parameter                              [pixel**(-2)]
466         if(m_ProcessType == EXTRACT_WITH_HFR)
467             out << "FLUX_RADIUS\n";//              Fraction-of-light radii                                   [pixel]
468         paramFile.close();
469     }
470     sextractorArgs << "-PARAMETERS_NAME" << paramPath;
471 
472 
473     //sextractor needs a default.conv file in the working directory
474     //This creates the default one
475 
476     QString convPath =  m_BasePath + "/" + m_BaseName + ".conv";
477     QFile convFile(convPath);
478     if (convFile.open(QIODevice::WriteOnly) == false)
479     {
480         QMessageBox::critical(nullptr, "Message", "Sextractor CONV filter write error.");
481         return -1;
482     }
483     else
484     {
485         QTextStream out(&convFile);
486         out << "CONV Filter Generated by StellarSolver Internal Library\n";
487         int c = 0;
488         for(int i = 0; i < m_ActiveParameters.convFilter.size(); i++)
489         {
490             out << m_ActiveParameters.convFilter.at(i);
491 
492             //We want the last one before the sqrt of the size to spark a new line
493             if(c < sqrt(m_ActiveParameters.convFilter.size()) - 1)
494             {
495                 out << " ";
496                 c++;
497             }
498             else
499             {
500                 out << "\n";
501                 c = 0;
502             }
503         }
504         convFile.close();
505     }
506 
507     //Arguments from the default.sex file
508     //------------------------------- Extraction ----------------------------------
509     sextractorArgs << "-DETECT_TYPE" << "CCD";
510     sextractorArgs << "-DETECT_MINAREA" << QString::number(m_ActiveParameters.minarea);
511 
512     //sextractorArgs << "-DETECT_THRESH" << QString::number();
513     //sextractorArgs << "-ANALYSIS_THRESH" << QString::number(minarea);
514 
515     sextractorArgs << "-FILTER" << "Y";
516     sextractorArgs << "-FILTER_NAME" << convPath;
517 
518     sextractorArgs << "-DEBLEND_NTHRESH" << QString::number(m_ActiveParameters.deblend_thresh);
519     sextractorArgs << "-DEBLEND_MINCONT" << QString::number(m_ActiveParameters.deblend_contrast);
520 
521     sextractorArgs << "-CLEAN" << ((m_ActiveParameters.clean == 1) ? "Y" : "N");
522     sextractorArgs << "-CLEAN_PARAM" << QString::number(m_ActiveParameters.clean_param);
523 
524     //------------------------------ Photometry -----------------------------------
525     sextractorArgs << "-PHOT_AUTOPARAMS" << QString::number(m_ActiveParameters.kron_fact) + "," + QString::number(
526                        m_ActiveParameters.r_min);
527     sextractorArgs << "-MAG_ZEROPOINT" << QString::number(m_ActiveParameters.magzero);
528 
529     sextractorArgs <<  fileToProcess;
530 
531     sextractorProcess.clear();
532     sextractorProcess = new QProcess();
533 
534     sextractorProcess->setWorkingDirectory(m_BasePath);
535     sextractorProcess->setProcessChannelMode(QProcess::MergedChannels);
536     if(m_SSLogLevel != LOG_OFF)
537         connect(sextractorProcess, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSextractor);
538 
539     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
540     emit logOutput("Starting external sextractor with the " + m_ActiveParameters.listName + " profile...");
541     emit logOutput(sextractorBinaryPath + " " + sextractorArgs.join(' '));
542 
543     sextractorProcess->start(sextractorBinaryPath, sextractorArgs);
544     sextractorProcess->waitForFinished(30000); //Will timeout after 30 seconds
545     emit logOutput(sextractorProcess->readAllStandardError().trimmed());
546 
547     if(sextractorProcess->exitCode() != 0 || sextractorProcess->exitStatus() == QProcess::CrashExit)
548         return sextractorProcess->exitCode();
549 
550     int exitCode = getStarsFromXYLSFile();
551     if(exitCode != 0)
552         return exitCode;
553 
554     if(m_UseSubframe)
555     {
556         for(int i = 0; i < m_ExtractedStars.size(); i++)
557         {
558             FITSImage::Star star = m_ExtractedStars.at(i);
559             if(!m_SubFrameRect.contains(star.x, star.y))
560             {
561                 m_ExtractedStars.removeAt(i);
562                 i--;
563             }
564         }
565     }
566 
567     applyStarFilters(m_ExtractedStars);
568 
569     m_HasExtracted = true;
570 
571     return 0;
572 
573 }
574 
575 //The code for this method is copied and pasted and modified from OfflineAstrometryParser in KStars
576 //It runs the astrometry.net external program using the options selected.
runExternalSolver()577 int ExternalSextractorSolver::runExternalSolver()
578 {
579     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
580     if(m_ExtractorType == EXTRACTOR_BUILTIN)
581         emit logOutput("Configuring external Astrometry.net solver classically: using python and without Sextractor first");
582     else
583         emit logOutput("Configuring external Astrometry.net solver using an xylist input");
584 
585     if(m_ExtractorType == EXTRACTOR_BUILTIN)
586     {
587         QFileInfo file(fileToProcess);
588         if(!file.exists())
589         {
590             emit logOutput("The requested file to solve does not exist");
591             return -1;
592         }
593 
594         if( !isChildSolver && onlySendFITSFiles && file.suffix() != "fits" && file.suffix() != "fit")
595         {
596             int ret = saveAsFITS();
597             if(ret != 0)
598             {
599                 emit logOutput("Failed to Save the image as a FITS File.");
600                 return ret;
601             }
602         }
603         else
604         {
605             //Making a copy of the file and putting it in the temp directory
606             //So that we can find all the temporary files and delete them later
607             //That way we don't pollute the directory the original image is located in
608             QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
609             QFile::copy(fileToProcess, newFileURL);
610             fileToProcess = newFileURL;
611             fileToProcessIsTempFile = true;
612         }
613     }
614     else
615     {
616         QFileInfo sextractorFile(sextractorFilePath);
617         if(!sextractorFile.exists())
618         {
619             emit logOutput("Please Sextract the image first");
620         }
621         if(isChildSolver)
622         {
623             QString newFileURL = m_BasePath + "/" + m_BaseName + "." + sextractorFile.suffix();
624             QFile::copy(sextractorFilePath, newFileURL);
625             sextractorFilePath = newFileURL;
626             sextractorFilePathIsTempFile = true;
627         }
628     }
629 
630     QStringList solverArgs = getSolverArgsList();
631 
632     if(m_ExtractorType == EXTRACTOR_BUILTIN)
633     {
634         solverArgs << "--keep-xylist" << sextractorFilePath;
635         solverArgs << fileToProcess;
636     }
637     else
638         solverArgs << sextractorFilePath;
639 
640     solver.clear();
641     solver = new QProcess();
642 
643     solver->setProcessChannelMode(QProcess::MergedChannels);
644     if(m_AstrometryLogLevel != LOG_NONE)
645         connect(solver, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSolver);
646 
647 #ifdef _WIN32 //This will set up the environment so that the ANSVR internal solver will work when started from this program.  This is needed for all types of astrometry solvers using ANSVR
648     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
649     QString path            = env.value("Path", "");
650     QString ansvrPath = QDir::homePath() + "/AppData/Local/cygwin_ansvr/";
651     QString pathsToInsert = ansvrPath + "bin;";
652     pathsToInsert += ansvrPath + "lib/lapack;";
653     pathsToInsert += ansvrPath + "lib/astrometry/bin;";
654     env.insert("Path", pathsToInsert + path);
655     solver->setProcessEnvironment(env);
656 #endif
657 
658 #ifdef Q_OS_OSX //This is needed so that astrometry.net can find netpbm and python on Mac when started from this program.  It is not needed when using an alternate sextractor
659     if(m_ExtractorType == EXTRACTOR_BUILTIN && m_SolverType == SOLVER_LOCALASTROMETRY)
660     {
661         QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
662         QString path            = env.value("PATH", "");
663         QString pythonExecPath = "/usr/local/opt/python/libexec/bin";
664         env.insert("PATH", "/Applications/KStars.app/Contents/MacOS/netpbm/bin:" + pythonExecPath + ":/usr/local/bin:" + path);
665         solver->setProcessEnvironment(env);
666     }
667 #endif
668 
669     solver->start(solverPath, solverArgs);
670     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
671     emit logOutput("Starting external Astrometry.net solver with the " + m_ActiveParameters.listName + " profile...");
672     emit logOutput("Command: " + solverPath + " " + solverArgs.join(" "));
673 
674     solver->waitForFinished(m_ActiveParameters.solverTimeLimit * 1000 *
675                             1.2); //Set to timeout in a little longer than the timeout
676     if(solver->error() == QProcess::Timedout)
677     {
678         emit logOutput("Solver timed out, aborting");
679         abort();
680         return solver->exitCode();
681     }
682     if(solver->exitCode() != 0)
683         return solver->exitCode();
684     if(solver->exitStatus() == QProcess::CrashExit)
685         return -1;
686     if(m_WasAborted)
687         return -1;
688     if(!getSolutionInformation())
689         return -1;
690     loadWCS(); //Attempt to Load WCS, but don't totally fail if you don't find it.
691     m_HasSolved = true;
692     return 0;
693 }
694 
695 //The code for this method is copied and pasted and modified from OfflineAstrometryParser in KStars
696 //It runs the astrometry.net external program using the options selected.
runExternalASTAPSolver()697 int ExternalSextractorSolver::runExternalASTAPSolver()
698 {
699     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
700     emit logOutput("Configuring external ASTAP solver");
701 
702     if(m_ExtractorType != EXTRACTOR_BUILTIN)
703     {
704         QFileInfo file(fileToProcess);
705         if(!file.exists())
706             return -1;
707 
708         QString newFileURL = m_BasePath + "/" + m_BaseName + "." + file.suffix();
709         QFile::copy(fileToProcess, newFileURL);
710         fileToProcess = newFileURL;
711         fileToProcessIsTempFile = true;
712     }
713 
714     QStringList solverArgs;
715 
716     QString astapSolutionFile = m_BasePath + "/" + m_BaseName + ".ini";
717     solverArgs << "-o" << astapSolutionFile;
718     solverArgs << "-speed" << "auto";
719     solverArgs << "-f" << fileToProcess;
720     solverArgs << "-wcs";
721     if(m_ActiveParameters.downsample > 1)
722         solverArgs << "-z" << QString::number(m_ActiveParameters.downsample);
723     else
724         solverArgs << "-z" << "0";
725     if(m_UseScale)
726     {
727         double scalemid = (scalehi + scalelo) / 2;
728         double degreesFOV = convertToDegreeHeight(scalemid);
729         solverArgs << "-fov" << QString::number(degreesFOV);
730     }
731     if(m_UsePosition)
732     {
733         solverArgs << "-ra" << QString::number(search_ra / 15.0); //Convert ra to hours
734         solverArgs << "-spd" << QString::number(search_dec + 90); //Convert dec to spd
735         solverArgs << "-r" << QString::number(m_ActiveParameters.search_radius);
736     }
737     if(m_AstrometryLogLevel != LOG_NONE)
738         solverArgs << "-log";
739 
740     solver.clear();
741     solver = new QProcess();
742 
743     solver->setProcessChannelMode(QProcess::MergedChannels);
744     if(m_AstrometryLogLevel != LOG_NONE)
745         connect(solver, &QProcess::readyReadStandardOutput, this, &ExternalSextractorSolver::logSolver);
746 
747     solver->start(astapBinaryPath, solverArgs);
748 
749     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
750     emit logOutput("Starting external ASTAP Solver with the " + m_ActiveParameters.listName + " profile...");
751     emit logOutput("Command: " + astapBinaryPath + " " + solverArgs.join(" "));
752 
753     solver->waitForFinished(m_ActiveParameters.solverTimeLimit * 1000 *
754                             1.2); //Set to timeout in a little longer than the timeout
755 
756     if(m_AstrometryLogLevel != LOG_NONE)
757     {
758         QFile logFile(m_BasePath + "/" + m_BaseName + ".log");
759         if(logFile.exists())
760         {
761             if(m_LogToFile)
762             {
763                 if(m_LogFileName != "")
764                     logFile.copy(m_LogFileName);
765             }
766             else
767             {
768                 if (logFile.open(QIODevice::ReadOnly))
769                 {
770                     QTextStream in(&logFile);
771                     emit logOutput(in.readAll());
772                 }
773                 else
774                     emit logOutput("Failed to open ASTAP log file" + logFile.fileName());
775             }
776         }
777         else
778             emit logOutput("ASTAP log file " + logFile.fileName() + " does not exist.");
779     }
780 
781     if(solver->error() == QProcess::Timedout)
782     {
783         emit logOutput("Solver timed out, aborting");
784         abort();
785         return solver->exitCode();
786     }
787     if(solver->exitCode() != 0)
788         return solver->exitCode();
789     if(solver->exitStatus() == QProcess::CrashExit)
790         return -1;
791     if(!getASTAPSolutionInformation())
792         return -1;
793     loadWCS(); //Attempt to Load WCS, but don't totally fail if you don't find it.
794     m_HasSolved = true;
795     return 0;
796 }
797 
798 //This method is copied and pasted and modified from getSolverOptionsFromFITS in Align in KStars
799 //Then it was split in two parts (The other part is located in the MainWindow class)
800 //This part generates the argument list from the options for the external solver only
getSolverArgsList()801 QStringList ExternalSextractorSolver::getSolverArgsList()
802 {
803     QStringList solverArgs;
804 
805     // Start with always-used arguments.  The way we are using Astrometry.net,
806     //We really would prefer that it always overwrite existing files, that it not waste any time
807     //writing plots to a file, and that it doesn't run the verification.
808     //We would also like the Coordinates found to be the center of the image
809     solverArgs << "-O" << "--no-plots" << "--no-verify" << "--crpix-center";
810 
811     //We would also prefer that it not write these temporary files, because they are a waste of
812     //resources, time, hard disk space, and memory card life since we aren't using them.
813     solverArgs << "--match" << "none";
814     solverArgs << "--corr" << "none";
815     solverArgs << "--new-fits" << "none";
816     solverArgs << "--rdls" << "none";
817 
818     //This parameter controls whether to resort the stars or not.
819     if(m_ActiveParameters.resort)
820         solverArgs << "--resort";
821 
822     if(depthhi != -1 && depthlo != -1)
823         solverArgs << "--depth" << QString("%1-%2").arg(depthlo).arg(depthhi);
824 
825     if(m_ActiveParameters.keepNum != 0)
826         solverArgs << "--objs" << QString("%1").arg(m_ActiveParameters.keepNum);
827 
828     //This will shrink the image so that it is easier to solve.  It is only useful if you are sending an image.
829     //It is not used if you are solving an xylist as in the classic astrometry.net solver
830     if(m_ActiveParameters.downsample > 1 && m_ExtractorType == EXTRACTOR_BUILTIN)
831         solverArgs << "--downsample" << QString::number(m_ActiveParameters.downsample);
832 
833     //I am not sure if we want to provide these options or not.  They do make a huge difference in solving time
834     //But changing them can be dangerous because it can cause false positive solves.
835     solverArgs << "--odds-to-solve" << QString::number(exp(m_ActiveParameters.logratio_tosolve));
836     solverArgs << "--odds-to-tune-up" << QString::number(exp(m_ActiveParameters.logratio_totune));
837     //solverArgs << "--odds-to-keep" << QString::number(logratio_tokeep);  I'm not sure if this is one we need.
838 
839     if (m_UseScale)
840         solverArgs << "-L" << QString::number(scalelo) << "-H" << QString::number(scalehi) << "-u" << getScaleUnitString();
841 
842     if (m_UsePosition)
843         solverArgs << "-3" << QString::number(search_ra) << "-4" << QString::number(search_dec) << "-5" << QString::number(
844                        m_ActiveParameters.search_radius);
845 
846     //The following options are unnecessary if you are sending an image to Astrometry.net
847     //And not an xylist
848     if(m_ExtractorType != EXTRACTOR_BUILTIN)
849     {
850         //These options are required to use an xylist, so all solvers that don't send an image
851         //to Astrometry.net must send these 4 options.
852         solverArgs << "--width" << QString::number(m_Statistics.width);
853         solverArgs << "--height" << QString::number(m_Statistics.height);
854         solverArgs << "--x-column" << "X_IMAGE";
855         solverArgs << "--y-column" << "Y_IMAGE";
856 
857         //These sort options are required to sort when you have an xylist input
858         //We only want to send them if the resort option is selected though.
859         if(m_ActiveParameters.resort)
860         {
861             solverArgs << "--sort-column" << "MAG_AUTO";
862             solverArgs << "--sort-ascending";
863         }
864     }
865 
866     //Note This set of items is NOT NEEDED for Sextractor or for astrometry.net to solve, it is needed to avoid python usage.
867     //On many user's systems especially on Mac and sometimes on Windows, there is an issue in the Python setup that causes astrometry to fail.
868     //This should avoid those problems as long as you send a FITS file or a xy list to astrometry.
869     solverArgs << "--no-remove-lines";
870     solverArgs << "--uniformize" << "0";
871     if(onlySendFITSFiles && m_ExtractorType == EXTRACTOR_BUILTIN)
872         solverArgs << "--fits-image";
873 
874     //Don't need any argument for default level
875     if(m_AstrometryLogLevel == LOG_MSG || m_AstrometryLogLevel == LOG_ERROR)
876         solverArgs << "-v";
877     else if(m_AstrometryLogLevel == LOG_VERB || m_AstrometryLogLevel == LOG_ALL)
878         solverArgs << "-vv";
879 
880     if(autoGenerateAstroConfig || !QFile(confPath).exists())
881         generateAstrometryConfigFile();
882 
883     //This sends the path to the config file.  Note that backend-config seems to be more universally recognized across
884     //the different solvers than config
885     solverArgs << "--backend-config" << confPath;
886 
887     //This sets the cancel filename for astrometry.net.  Astrometry will monitor for the creation of this file
888     //In order to shut down and stop processing
889     solverArgs << "--cancel" << cancelfn;
890 
891     //This sets the wcs file for astrometry.net.  This file will be very important for reading in WCS info later on
892     solverArgs << "-W" << solutionFile;
893 
894     return solverArgs;
895 }
896 
897 //This will generate a temporary Astrometry.cfg file to use for solving so that we have more control over these options
898 //for the external solvers from inside the program.
generateAstrometryConfigFile()899 bool ExternalSextractorSolver::generateAstrometryConfigFile()
900 {
901     confPath =  m_BasePath + "/" + m_BaseName + ".cfg";
902     QFile configFile(confPath);
903     if (configFile.open(QIODevice::WriteOnly) == false)
904     {
905         QMessageBox::critical(nullptr, "Message", "Config file write error.");
906         return false;
907     }
908     else
909     {
910         QTextStream out(&configFile);
911         if(m_ActiveParameters.inParallel)
912             out << "inparallel\n";
913         out << "minwidth " << m_ActiveParameters.minwidth << "\n";
914         out << "maxwidth " << m_ActiveParameters.maxwidth << "\n";
915         out << "cpulimit " << m_ActiveParameters.solverTimeLimit << "\n";
916         out << "autoindex\n";
917         foreach(QString folder, indexFolderPaths)
918         {
919             out << "add_path " << folder << "\n";
920         }
921         configFile.close();
922     }
923     return true;
924 }
925 
926 
927 //These methods are for the logging of information to the textfield at the bottom of the window.
928 
logSextractor()929 void ExternalSextractorSolver::logSextractor()
930 {
931     if(sextractorProcess->canReadLine())
932     {
933         QString rawText(sextractorProcess->readLine().trimmed());
934         QString cleanedString = rawText.remove("[1M>").remove("[1A");
935         if(!cleanedString.isEmpty())
936         {
937             emit logOutput(cleanedString);
938             if(m_LogToFile)
939             {
940                 QFile file(m_LogFileName);
941                 if (file.open(QIODevice::Append | QIODevice::Text))
942                 {
943                     QTextStream outstream(&file);
944                     outstream << cleanedString << endl;
945                     file.close();
946                 }
947                 else
948                     emit logOutput(("Log File Write Error"));
949             }
950         }
951     }
952 }
953 
logSolver()954 void ExternalSextractorSolver::logSolver()
955 {
956     if(solver->canReadLine())
957     {
958         QString solverLine(solver->readLine().trimmed());
959         if(!solverLine.isEmpty())
960         {
961             emit logOutput(solverLine);
962             if(m_LogToFile)
963             {
964                 QFile file(m_LogFileName);
965                 if (file.open(QIODevice::Append | QIODevice::Text))
966                 {
967                     QTextStream outstream(&file);
968                     outstream << solverLine << endl;
969                     file.close();
970                 }
971                 else
972                     emit logOutput(("Log File Write Error"));
973             }
974         }
975     }
976 }
977 
978 //This method is copied and pasted and modified from tablist.c in astrometry.net
979 //This is needed to load in the stars sextracted by an extrnal sextractor to get them into the table
getStarsFromXYLSFile()980 int ExternalSextractorSolver::getStarsFromXYLSFile()
981 {
982     QFile sextractorFile(sextractorFilePath);
983     if(!sextractorFile.exists())
984     {
985         emit logOutput("Can't get sextractor file since it doesn't exist.");
986         return -1;
987     }
988 
989     fitsfile * new_fptr;
990     char error_status[512];
991 
992     /* FITS file pointer, defined in fitsio.h */
993     char *val, value[1000], nullptrstr[] = "*";
994     int status = 0;   /*  CFITSIO status value MUST be initialized to zero!  */
995     int hdunum, hdutype = ANY_HDU, ncols, ii, anynul;
996     long nelements[1000];
997     long jj, nrows, kk;
998 
999     if (fits_open_diskfile(&new_fptr, sextractorFilePath.toLatin1(), READONLY, &status))
1000     {
1001         fits_report_error(stderr, status);
1002         fits_get_errstatus(status, error_status);
1003         emit logOutput(QString::fromUtf8(error_status));
1004         return status;
1005     }
1006 
1007     if ( fits_get_hdu_num(new_fptr, &hdunum) == 1 )
1008         /* This is the primary array;  try to move to the */
1009         /* first extension and see if it is a table */
1010         fits_movabs_hdu(new_fptr, 2, &hdutype, &status);
1011     else
1012         fits_get_hdu_type(new_fptr, &hdutype, &status); /* Get the HDU type */
1013 
1014     if (!(hdutype == ASCII_TBL || hdutype == BINARY_TBL))
1015     {
1016         emit logOutput("Wrong type of file");
1017         return -1;
1018     }
1019 
1020     fits_get_num_rows(new_fptr, &nrows, &status);
1021     fits_get_num_cols(new_fptr, &ncols, &status);
1022 
1023     for (jj = 1; jj <= ncols; jj++)
1024         fits_get_coltype(new_fptr, jj, nullptr, &nelements[jj], nullptr, &status);
1025 
1026     m_ExtractedStars.clear();
1027 
1028     /* read each column, row by row */
1029     val = value;
1030     for (jj = 1; jj <= nrows && !status; jj++)
1031     {
1032         float starx = 0;
1033         float stary = 0;
1034         float mag = 0;
1035         float flux = 0;
1036         float peak = 0;
1037         float xx = 0;
1038         float yy = 0;
1039         float xy = 0;
1040         float HFR = 0;
1041 
1042         for (ii = 1; ii <= ncols; ii++)
1043         {
1044             for (kk = 1; kk <= nelements[ii]; kk++)
1045             {
1046                 /* read value as a string, regardless of intrinsic datatype */
1047                 if (fits_read_col_str (new_fptr, ii, jj, kk, 1, nullptrstr,
1048                                        &val, &anynul, &status) )
1049                     break;  /* jump out of loop on error */
1050                 if(m_SolverType == SOLVER_LOCALASTROMETRY || m_SolverType == SOLVER_ONLINEASTROMETRY)
1051                 {
1052                     if(ii == 1)
1053                         starx = QString(value).trimmed().toFloat();
1054                     if(ii == 2)
1055                         stary = QString(value).trimmed().toFloat();
1056                     if(ii == 3)
1057                         flux = QString(value).trimmed().toFloat();
1058                 }
1059                 else if(m_SolverType == SOLVER_ASTAP)
1060                 {
1061                     if(ii == 1)
1062                         starx = QString(value).trimmed().toFloat();
1063                     if(ii == 2)
1064                         stary = QString(value).trimmed().toFloat();
1065                 }
1066                 else
1067                 {
1068                     if(ii == 1)
1069                         starx = QString(value).trimmed().toFloat();
1070                     if(ii == 2)
1071                         stary = QString(value).trimmed().toFloat();
1072                     if(ii == 3)
1073                         mag = QString(value).trimmed().toFloat();
1074                     if(ii == 4)
1075                         flux = QString(value).trimmed().toFloat();
1076                     if(ii == 5)
1077                         peak = QString(value).trimmed().toFloat();
1078                     if(ii == 6)
1079                         xx = QString(value).trimmed().toFloat();
1080                     if(ii == 7)
1081                         yy = QString(value).trimmed().toFloat();
1082                     if(ii == 8)
1083                         xy = QString(value).trimmed().toFloat();
1084                     if(m_ProcessType == EXTRACT_WITH_HFR && ii == 9)
1085                         HFR = QString(value).trimmed().toFloat();
1086                 }
1087             }
1088         }
1089 
1090         //  xx  xy      or     a   b
1091         //  xy  yy             b   c
1092         //Note, I got this translation from these two sources which agree:
1093         //https://books.google.com/books?id=JNEn23UyHuAC&pg=PA84&lpg=PA84&dq=ellipse+xx+yy+xy&source=bl&ots=ynAWge4jlb&sig=ACfU3U1pqZTkx8Teu9pBTygI9F-WcTncrg&hl=en&sa=X&ved=2ahUKEwj0s-7C3I7oAhXblnIEHacAAf0Q6AEwBHoECAUQAQ#v=onepage&q=ellipse%20xx%20yy%20xy&f=false
1094         //https://cookierobotics.com/007/
1095         float a = 0;
1096         float b = 0;
1097         float theta = 0;
1098         if(m_SolverType != SOLVER_LOCALASTROMETRY && m_SolverType != SOLVER_ONLINEASTROMETRY && m_SolverType != SOLVER_ASTAP)
1099         {
1100             float thing = sqrt( pow(xx - yy, 2) + 4 * pow(xy, 2) );
1101             float lambda1 = (xx + yy + thing) / 2;
1102             float lambda2 = (xx + yy - thing) / 2;
1103             a = sqrt(lambda1);
1104             b = sqrt(lambda2);
1105             theta = qRadiansToDegrees(atan(xy / (lambda1 - yy)));
1106         }
1107 
1108         FITSImage::Star star = {starx, stary, mag, flux, peak, HFR, a, b, theta, 0, 0};
1109 
1110         m_ExtractedStars.append(star);
1111     }
1112     fits_close_file(new_fptr, &status);
1113 
1114     if (status) fits_report_error(stderr, status); /* print any error message */
1115 
1116     return 0;
1117 }
1118 
1119 //This method was based on a method in KStars.
1120 //It reads the information from the Solution file from Astrometry.net and puts it into the solution
getSolutionInformation()1121 bool ExternalSextractorSolver::getSolutionInformation()
1122 {
1123     if(solutionFile == "")
1124         solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
1125     QFileInfo solutionInfo(solutionFile);
1126     if(!solutionInfo.exists())
1127     {
1128         emit logOutput("Solution file doesn't exist");
1129         return false;
1130     }
1131     QProcess wcsProcess;
1132 
1133 #ifdef _WIN32 //This will set up the environment so that the ANSVR internal wcsinfo will work
1134     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
1135     QString path            = env.value("Path", "");
1136     QString ansvrPath = QDir::homePath() + "/AppData/Local/cygwin_ansvr/";
1137     QString pathsToInsert = ansvrPath + "bin;";
1138     pathsToInsert += ansvrPath + "lib/lapack;";
1139     pathsToInsert += ansvrPath + "lib/astrometry/bin;";
1140     env.insert("Path", pathsToInsert + path);
1141     wcsProcess.setProcessEnvironment(env);
1142 #endif
1143 
1144     wcsProcess.start(wcsPath, QStringList(solutionFile));
1145     wcsProcess.waitForFinished(30000); //Will timeout after 30 seconds
1146     QString wcsinfo_stdout = wcsProcess.readAllStandardOutput();
1147 
1148     //This is a quick way to find out what keys are available
1149     // emit logOutput(wcsinfo_stdout);
1150 
1151     QStringList wcskeys = wcsinfo_stdout.split(QRegExp("[\n]"));
1152 
1153     QStringList key_value;
1154 
1155     double ra = 0, dec = 0, orient = 0;
1156     double fieldw = 0, fieldh = 0, pixscale = 0;
1157     QString rastr, decstr;
1158     QString parity;
1159 
1160     for (auto &key : wcskeys)
1161     {
1162         key_value = key.split(' ');
1163 
1164         if (key_value.size() > 1)
1165         {
1166             if (key_value[0] == "ra_center")
1167                 ra = key_value[1].toDouble();
1168             else if (key_value[0] == "dec_center")
1169                 dec = key_value[1].toDouble();
1170             else if (key_value[0] == "orientation_center")
1171                 orient = key_value[1].toDouble();
1172             else if (key_value[0] == "fieldw")
1173                 fieldw = key_value[1].toDouble();
1174             else if (key_value[0] == "fieldh")
1175                 fieldh = key_value[1].toDouble();
1176             else if (key_value[0] == "ra_center_hms")
1177                 rastr = key_value[1];
1178             else if (key_value[0] == "dec_center_dms")
1179                 decstr = key_value[1];
1180             else if (key_value[0] == "pixscale")
1181                 pixscale = key_value[1].toDouble();
1182             else if (key_value[0] == "parity")
1183                 parity = (key_value[1].toInt() == -1) ? "pos" : "neg";
1184         }
1185     }
1186 
1187     if(usingDownsampledImage)
1188         pixscale /= m_ActiveParameters.downsample;
1189 
1190     double raErr = 0;
1191     double decErr = 0;
1192     if(m_UsePosition)
1193     {
1194         raErr = (search_ra - ra) * 3600;
1195         decErr = (search_dec - dec) * 3600;
1196     }
1197 
1198     m_Solution = {fieldw, fieldh, ra, dec, orient, pixscale, parity, raErr, decErr};
1199 
1200     emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
1201     emit logOutput(QString("Field center: (RA,Dec) = (%1, %2) deg.").arg( ra).arg( dec));
1202     emit logOutput(QString("Field center: (RA H:M:S, Dec D:M:S) = (%1, %2).").arg( rastr).arg( decstr));
1203     if(m_UsePosition)
1204         emit logOutput(QString("Field is: (%1, %2) deg from search coords.").arg( raErr).arg( decErr));
1205     emit logOutput(QString("Field size: %1 x %2 arcminutes").arg( fieldw).arg( fieldh));
1206     emit logOutput(QString("Pixel Scale: %1\"").arg( pixscale ));
1207     emit logOutput(QString("Field rotation angle: up is %1 degrees E of N").arg( orient));
1208     emit logOutput(QString("Field parity: %1\n").arg( parity));
1209 
1210     return true;
1211 }
1212 
1213 //This method was based on a method in KStars.
1214 //It reads the information from the Solution file from Astrometry.net and puts it into the solution
getASTAPSolutionInformation()1215 bool ExternalSextractorSolver::getASTAPSolutionInformation()
1216 {
1217     QFile results(m_BasePath + "/" + m_BaseName + ".ini");
1218 
1219     if (!results.open(QIODevice::ReadOnly))
1220     {
1221         emit logOutput("Failed to open solution file" + m_BasePath + "/" + m_BaseName + ".ini");
1222         return false;
1223     }
1224 
1225     QTextStream in(&results);
1226     QString line = in.readLine();
1227 
1228     QStringList ini = line.split("=");
1229     if(ini.count() <= 1)
1230     {
1231         emit logOutput("Results file is empty, try again.");
1232         return false;
1233     }
1234     if (ini[1] == "F")
1235     {
1236         line = in.readLine();
1237         //If the plate solve failed, we still need to search for any error or warning messages and print them out.
1238         while (!line.isNull())
1239         {
1240             QStringList ini = line.split("=");
1241             if (ini[0] == "WARNING")
1242                 emit logOutput(line.mid(8).trimmed());
1243             else if (ini[0] == "ERROR")
1244                 emit logOutput(line.mid(6).trimmed());
1245 
1246             line = in.readLine();
1247         }
1248         emit logOutput("Solver failed. Try again.");
1249         return false;
1250     }
1251     double ra = 0, dec = 0, orient = 0;
1252     double fieldw = 0, fieldh = 0, pixscale = 0;
1253     char rastr[32], decstr[32];
1254     QString parity = "";
1255     double cd11 = 0;
1256     double cd22 = 0;
1257     double cd12 = 0;
1258     double cd21 = 0;
1259     bool ok[8] = {false};
1260 
1261     line = in.readLine();
1262     while (!line.isNull())
1263     {
1264         QStringList ini = line.split("=");
1265         if (ini[0] == "CRVAL1")
1266             ra = ini[1].trimmed().toDouble(&ok[0]);
1267         else if (ini[0] == "CRVAL2")
1268             dec = ini[1].trimmed().toDouble(&ok[1]);
1269         else if (ini[0] == "CDELT1")
1270             pixscale = ini[1].trimmed().toDouble(&ok[2]) * 3600.0;
1271         else if (ini[0] == "CROTA2")
1272             orient = ini[1].trimmed().toDouble(&ok[3]);
1273         else if (ini[0] == "CD1_1")
1274             cd11 = ini[1].trimmed().toDouble(&ok[4]);
1275         else if (ini[0] == "CD1_2")
1276             cd12 = ini[1].trimmed().toDouble(&ok[5]);
1277         else if (ini[0] == "CD2_1")
1278             cd21 = ini[1].trimmed().toDouble(&ok[6]);
1279         else if (ini[0] == "CD2_2")
1280             cd22 = ini[1].trimmed().toDouble(&ok[7]);
1281         else if (ini[0] == "WARNING")
1282             emit logOutput(line.mid(8).trimmed());
1283         else if (ini[0] == "ERROR")
1284             emit logOutput(line.mid(6).trimmed());
1285 
1286         line = in.readLine();
1287     }
1288 
1289     if ( ok[0] && ok[1] && ok[2] && ok[3] )
1290     {
1291         ra2hmsstring(ra, rastr);
1292         dec2dmsstring(dec, decstr);
1293         fieldw = m_Statistics.width * pixscale / 60;
1294         fieldh = m_Statistics.height * pixscale / 60;
1295 
1296         if ( ok[4] && ok[5] && ok[6] && ok[7] )
1297         {
1298             // Note, negative determinant = positive parity.
1299             double det = cd11 * cd22 - cd12 * cd21;
1300             if(det > 0)
1301                 parity = "neg";
1302             else
1303                 parity = "pos";
1304         }
1305 
1306         double raErr = 0;
1307         double decErr = 0;
1308         if(m_UsePosition)
1309         {
1310             raErr = (search_ra - ra) * 3600;
1311             decErr = (search_dec - dec) * 3600;
1312         }
1313 
1314         m_Solution = {fieldw, fieldh, ra, dec, orient, pixscale, parity, raErr, decErr};
1315         emit logOutput("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
1316         emit logOutput(QString("Field center: (RA,Dec) = (%1, %2) deg.").arg( ra).arg( dec));
1317         emit logOutput(QString("Field center: (RA H:M:S, Dec D:M:S) = (%1, %2).").arg( rastr).arg( decstr));
1318         if(m_UsePosition)
1319             emit logOutput(QString("Field is: (%1, %2) deg from search coords.").arg( raErr).arg( decErr));
1320         emit logOutput(QString("Field size: %1 x %2 arcminutes").arg( fieldw).arg( fieldh));
1321         emit logOutput(QString("Pixel Scale: %1\"").arg( pixscale ));
1322         emit logOutput(QString("Field rotation angle: up is %1 degrees E of N").arg( orient));
1323         emit logOutput(QString("Field parity: %1\n").arg( parity));
1324 
1325         return true;
1326     }
1327     else
1328     {
1329         emit logOutput("Solver failed. Try again.");
1330         return false;
1331     }
1332 }
1333 
1334 //This method writes the table to the file
1335 //I had to create it from the examples on NASA's website
1336 //When I first made this program, I needed it to generate an xyls file from the internal sextraction
1337 //Now it is just used on Windows for the external solving because it needs to use the internal sextractor and the external solver.
1338 //https://heasarc.gsfc.nasa.gov/docs/software/fitsio/quick/node10.html
1339 //https://heasarc.gsfc.nasa.gov/docs/software/fitsio/cookbook/node16.html
writeSextractorTable()1340 int ExternalSextractorSolver::writeSextractorTable()
1341 {
1342     int status = 0;
1343     fitsfile * new_fptr;
1344 
1345     if(m_ExtractorType == EXTRACTOR_INTERNAL && m_SolverType == SOLVER_ASTAP)
1346     {
1347         QFileInfo file(fileToProcess);
1348         if(!file.exists())
1349             return -1;
1350 
1351         if(file.suffix() != "fits" && file.suffix() != "fit")
1352         {
1353             int ret = saveAsFITS();
1354             if(ret != 0)
1355                 return ret;
1356         }
1357         else
1358         {
1359             QString newFileURL = m_BasePath + "/" + m_BaseName + ".fits";
1360             QFile::copy(fileToProcess, newFileURL);
1361             fileToProcess = newFileURL;
1362             fileToProcessIsTempFile = true;
1363         }
1364 
1365         if (fits_open_diskfile(&new_fptr, fileToProcess.toLatin1(), READWRITE, &status))
1366         {
1367             fits_report_error(stderr, status);
1368             return status;
1369         }
1370     }
1371     else
1372     {
1373         if(sextractorFilePath == "")
1374         {
1375             sextractorFilePathIsTempFile = true;
1376             sextractorFilePath = m_BasePath + "/" + m_BaseName + ".xyls";
1377         }
1378 
1379         QFile sextractorFile(sextractorFilePath);
1380         if(sextractorFile.exists())
1381             sextractorFile.remove();
1382 
1383         if (fits_create_file(&new_fptr, sextractorFilePath.toLatin1(), &status))
1384         {
1385             fits_report_error(stderr, status);
1386             return status;
1387         }
1388     }
1389 
1390     int tfields = 3;
1391     int nrows = m_ExtractedStars.size();
1392     QString extname = "Sextractor_File";
1393 
1394     //Columns: X_IMAGE, double, pixels, Y_IMAGE, double, pixels, MAG_AUTO, double, mag
1395     char* ttype[] = { xcol, ycol, magcol };
1396     char* tform[] = { colFormat, colFormat, colFormat };
1397     char* tunit[] = { colUnits, colUnits, magUnits };
1398     const char* extfile = "Sextractor_File";
1399 
1400     float *xArray = new float[m_ExtractedStars.size()];
1401     float *yArray = new float[m_ExtractedStars.size()];
1402     float *magArray = new float[m_ExtractedStars.size()];
1403 
1404     for (int i = 0; i < m_ExtractedStars.size(); i++)
1405     {
1406         xArray[i] = m_ExtractedStars.at(i).x;
1407         yArray[i] = m_ExtractedStars.at(i).y;
1408         magArray[i] = m_ExtractedStars.at(i).mag;
1409     }
1410 
1411     int firstrow  = 1;  /* first row in table to write   */
1412     int firstelem = 1;
1413     int column = 1;
1414 
1415     if(fits_create_tbl(new_fptr, BINARY_TBL, nrows, tfields,
1416                        ttype, tform, tunit, extfile, &status))
1417     {
1418         emit logOutput(QString("Could not create binary table."));
1419         goto exit;
1420     }
1421 
1422     if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, xArray, &status))
1423     {
1424         emit logOutput(QString("Could not write x pixels in binary table."));
1425         goto exit;
1426     }
1427 
1428     column = 2;
1429     if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, yArray, &status))
1430     {
1431         emit logOutput(QString("Could not write y pixels in binary table."));
1432         goto exit;
1433     }
1434 
1435     column = 3;
1436     if(fits_write_col(new_fptr, TFLOAT, column, firstrow, firstelem, nrows, magArray, &status))
1437     {
1438         emit logOutput(QString("Could not write magnitudes in binary table."));
1439         goto exit;
1440     }
1441 
1442     if(fits_close_file(new_fptr, &status))
1443     {
1444         emit logOutput(QString("Error closing file."));
1445         goto exit;
1446     }
1447     status = 0;
1448 
1449     exit:
1450         delete[] xArray;
1451         delete[] yArray;
1452         delete[] magArray;
1453 
1454         return status;
1455 }
1456 
1457 //This is very necessary for solving non-fits images with external Sextractor
1458 //This was copied and pasted and modified from ImageToFITS in fitsdata in KStars
saveAsFITS()1459 int ExternalSextractorSolver::saveAsFITS()
1460 {
1461     QFileInfo fileInfo(fileToProcess.toLatin1());
1462     QString newFilename = m_BasePath + "/" + m_BaseName + ".fits";
1463 
1464     int status = 0;
1465     fitsfile * new_fptr;
1466 
1467     //I am hoping that this is correct.
1468     //I"m trying to set these two variables based on the ndim variable since this class doesn't have access to these variables.
1469     long naxis;
1470     int channels;
1471     if (m_Statistics.ndim < 3)
1472     {
1473         channels = 1;
1474         naxis = 2;
1475     }
1476     else
1477     {
1478         channels = 3;
1479         naxis = 3;
1480     }
1481 
1482     long nelements, exposure;
1483     long naxes[3] = { m_Statistics.width, m_Statistics.height, channels };
1484     char error_status[512] = {0};
1485 
1486     QFileInfo newFileInfo(newFilename);
1487     if(newFileInfo.exists())
1488         QFile(newFilename).remove();
1489 
1490     nelements = m_Statistics.samples_per_channel * channels;
1491 
1492     /* Create a new File, overwriting existing*/
1493     if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status))
1494     {
1495         fits_report_error(stderr, status);
1496         return status;
1497     }
1498 
1499     int bitpix;
1500     switch(m_Statistics.dataType)
1501     {
1502     case SEP_TBYTE:
1503         bitpix = BYTE_IMG;
1504         break;
1505     case TSHORT:
1506         bitpix = SHORT_IMG;
1507         break;
1508     case TUSHORT:
1509         bitpix = USHORT_IMG;
1510         break;
1511     case TLONG:
1512         bitpix = LONG_IMG;
1513         break;
1514     case TULONG:
1515         bitpix = ULONG_IMG;
1516         break;
1517     case TFLOAT:
1518         bitpix = FLOAT_IMG;
1519         break;
1520     case TDOUBLE:
1521         bitpix = DOUBLE_IMG;
1522         break;
1523     default:
1524         bitpix = BYTE_IMG;
1525     }
1526 
1527     fitsfile *fptr = new_fptr;
1528     if (fits_create_img(fptr, bitpix, naxis, naxes, &status))
1529     {
1530         emit logOutput(QString("fits_create_img failed: %1").arg(error_status));
1531         status = 0;
1532         fits_flush_file(fptr, &status);
1533         fits_close_file(fptr, &status);
1534         return status;
1535     }
1536 
1537     /* Write Data */
1538     if (fits_write_img(fptr, m_Statistics.dataType, 1, nelements, const_cast<void *>(reinterpret_cast<const void *>(m_ImageBuffer)), &status))
1539     {
1540         fits_report_error(stderr, status);
1541         return status;
1542     }
1543 
1544     /* Write keywords */
1545 
1546     exposure = 1;
1547     fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);
1548 
1549     // NAXIS1
1550     if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(m_Statistics.width), "length of data axis 1", &status))
1551     {
1552         fits_report_error(stderr, status);
1553         return status;
1554     }
1555 
1556     // NAXIS2
1557     if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(m_Statistics.height), "length of data axis 2", &status))
1558     {
1559         fits_report_error(stderr, status);
1560         return status;
1561     }
1562 
1563     // ISO Date
1564     if (fits_write_date(fptr, &status))
1565     {
1566         fits_report_error(stderr, status);
1567         return status;
1568     }
1569 
1570     fileToProcess = newFilename;
1571     fileToProcessIsTempFile = true;
1572 
1573     fits_flush_file(fptr, &status);
1574 
1575     if(fits_close_file(fptr, &status))
1576     {
1577         emit logOutput(QString("Error closing file."));
1578         return status;
1579     }
1580 
1581     emit logOutput("Saved FITS file:" + fileToProcess);
1582 
1583     return 0;
1584 }
1585 
1586 //This was essentially copied from KStars' loadWCS method and split in half with some modifications.
loadWCS()1587 int ExternalSextractorSolver::loadWCS()
1588 {
1589     if(solutionFile == "")
1590         solutionFile = m_BasePath + "/" + m_BaseName + ".wcs";
1591 
1592     emit logOutput("Loading WCS from file...");
1593 
1594     QFile solution(solutionFile);
1595     if(!solution.exists())
1596     {
1597         emit logOutput("WCS File does not exist.");
1598         return -1;
1599     }
1600 
1601     int status = 0;
1602     char * header { nullptr };
1603     int nkeyrec, nreject, nwcs;
1604 
1605     fitsfile *fptr { nullptr };
1606 
1607     if (fits_open_diskfile(&fptr, solutionFile.toLatin1(), READONLY, &status))
1608     {
1609         char errmsg[512];
1610         fits_get_errstatus(status, errmsg);
1611         emit logOutput(QString("Error opening fits file %1, %2").arg(solutionFile).arg(errmsg));
1612         return status;
1613     }
1614 
1615     if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status))
1616     {
1617         char errmsg[512];
1618         fits_get_errstatus(status, errmsg);
1619         emit logOutput(QString("ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]));
1620         return status;
1621     }
1622 
1623     if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &m_wcs)) != 0)
1624     {
1625         free(header);
1626         wcsvfree(&m_nwcs, &m_wcs);
1627         m_wcs = nullptr;
1628         m_HasWCS = false;
1629         emit logOutput(QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]));
1630         return status;
1631     }
1632     fits_close_file(fptr, &status);
1633 
1634 #ifndef _WIN32 //For some very strange reason, this causes a crash on Windows??
1635     free(header);
1636 #endif
1637 
1638     if (m_wcs == nullptr)
1639     {
1640         emit logOutput("No world coordinate systems found.");
1641         m_HasWCS = false;
1642         return status;
1643     }
1644     else
1645         m_HasWCS = true;
1646 
1647     // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now.
1648     if (m_wcs->crpix[0] == 0)
1649     {
1650         wcsvfree(&m_nwcs, &m_wcs);
1651         m_wcs = nullptr;
1652         m_HasWCS = false;
1653         emit logOutput("No world coordinate systems found.");
1654         return status;
1655     }
1656 
1657     if ((status = wcsset(m_wcs)) != 0)
1658     {
1659         wcsvfree(&m_nwcs, &m_wcs);
1660         m_wcs = nullptr;
1661         m_HasWCS = false;
1662         emit logOutput(QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1663         return status;
1664     }
1665 
1666     emit logOutput("Finished Loading WCS...");
1667 
1668     return 0;
1669 }
1670 
1671 //This was essentially copied from KStars' loadWCS method and split in half with some modifications
computeWCSCoord()1672 void ExternalSextractorSolver::computeWCSCoord()
1673 {
1674     if(!m_HasWCS)
1675     {
1676         emit logOutput("There is no WCS Data.  Did you solve the image first?");
1677         return;
1678     }
1679     int w  = m_Statistics.width;
1680     int h = m_Statistics.height;
1681     wcs_coord = new FITSImage::wcs_point[w * h];
1682     FITSImage::wcs_point * p = wcs_coord;
1683     double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2];
1684     int status;
1685     int stat[2];
1686 
1687     for (int i = 0; i < h; i++)
1688     {
1689         for (int j = 0; j < w; j++)
1690         {
1691             pixcrd[0] = j;
1692             pixcrd[1] = i;
1693 
1694             if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0)
1695             {
1696                 emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1697             }
1698             else
1699             {
1700                 p->ra  = world[0];
1701                 p->dec = world[1];
1702 
1703                 p++;
1704             }
1705         }
1706     }
1707 }
1708 
pixelToWCS(const QPointF & pixelPoint,FITSImage::wcs_point & skyPoint)1709 bool ExternalSextractorSolver::pixelToWCS(const QPointF &pixelPoint, FITSImage::wcs_point &skyPoint)
1710 {
1711     if(!hasWCSData())
1712     {
1713         emit logOutput("There is no WCS Data.");
1714         return false;
1715     }
1716     double imgcrd[2], phi, pixcrd[2], theta, world[2];
1717     int stat[2];
1718     pixcrd[0] = pixelPoint.x();
1719     pixcrd[1] = pixelPoint.y();
1720 
1721     int status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0]);
1722     if(status != 0)
1723     {
1724         emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1725         return false;
1726     }
1727     else
1728     {
1729         skyPoint.ra = world[0];
1730         skyPoint.dec = world[1];
1731     }
1732     return true;
1733 }
1734 
wcsToPixel(const FITSImage::wcs_point & skyPoint,QPointF & pixelPoint)1735 bool ExternalSextractorSolver::wcsToPixel(const FITSImage::wcs_point &skyPoint, QPointF &pixelPoint)
1736 {
1737     if(!hasWCSData())
1738     {
1739         emit logOutput("There is no WCS Data.");
1740         return false;
1741     }
1742     double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2];
1743     int stat[2];
1744     worldcrd[0] = skyPoint.ra;
1745     worldcrd[1] = skyPoint.dec;
1746 
1747     int status = wcss2p(m_wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0]);
1748     if(status != 0)
1749     {
1750         emit logOutput(QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1751         return false;
1752     }
1753     pixelPoint.setX(pixcrd[0]);
1754     pixelPoint.setY(pixcrd[1]);
1755     return true;
1756 }
1757 
appendStarsRAandDEC(QList<FITSImage::Star> & stars)1758 bool ExternalSextractorSolver::appendStarsRAandDEC(QList<FITSImage::Star> &stars)
1759 {
1760     if(!m_HasWCS)
1761     {
1762         emit logOutput("There is no WCS Data.  Did you solve the image first?");
1763         return false;
1764     }
1765 
1766     double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2];
1767     int stat[2];
1768 
1769     for(auto &oneStar : stars)
1770     {
1771         int status = 0;
1772         double ra = HUGE_VAL;
1773         double dec = HUGE_VAL;
1774         pixcrd[0] = oneStar.x;
1775         pixcrd[1] = oneStar.y;
1776 
1777         if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0)
1778         {
1779             emit logOutput(QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]));
1780             return false;
1781         }
1782         else
1783         {
1784             ra  = world[0];
1785             dec = world[1];
1786         }
1787 
1788         oneStar.ra = ra;
1789         oneStar.dec = dec;
1790     }
1791 
1792     return true;
1793 }
1794