#include "scriptengine.h" #include #include #include #include #include "loadrunable.h" #include "rawimage.h" #include "loadrunable.h" #include "batchprocessing.h" #include #include "libXISF/libxisf.h" #ifdef PLATESOLVER #include "solver.h" #endif // PLATESOLVER namespace Script { ScriptEngine::ScriptEngine(BatchProcessing *parent) : _jsEngine(new QJSEngine(this)) , _database(new Database(this)) , _parent(parent) , _pool(new QThreadPool(this)) { QJSValue core = _jsEngine->newQObject(this); _jsEngine->globalObject().setProperty("core", core); QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject); _jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject); _database->init(QLatin1String("scriptengine")); _semaphore.release(_pool->maxThreadCount()); #ifdef PLATESOLVER _solver = new Solver(this); #endif // PLATESOLVER } void ScriptEngine::setParams(const QString &scriptPath, const QList> &paths, const QString &outputDir) { _scriptPath = scriptPath; _paths = paths; _outputDir = outputDir + "/"; } void ScriptEngine::reportError(const QString &message) { _jsEngine->throwError(message); } const QString &ScriptEngine::outputDir() const { return _outputDir; } void ScriptEngine::interrupt() { #ifdef PLATESOLVER if(_solver)_solver->abort(); #endif _jsEngine->setInterrupted(true); } void ScriptEngine::logError(const QString &message) { emit newMessage(message, true); } void ScriptEngine::log(const QString &message) { emit newMessage(message, false); } void ScriptEngine::mark(File *file) { _database->mark(file->absoluteFilePath()); } void ScriptEngine::unmark(File *file) { _database->unmark(file->absoluteFilePath()); } bool ScriptEngine::isMarked(const File *file) const { return _database->isMarked(file->absoluteFilePath()); } void ScriptEngine::setMaxThread(int maxthread) { int newval = std::max(std::min(QThread::idealThreadCount(), maxthread), 1); int oldval = _pool->maxThreadCount(); if(newval > oldval) _semaphore.release(newval - oldval); else if(newval < oldval) _semaphore.acquire(oldval - newval); _pool->setMaxThreadCount(newval); } void ScriptEngine::sync() { _pool->waitForDone(); } QJSValue ScriptEngine::getString(const QString &label, const QString &text) const { QJSValue ret; QMetaObject::invokeMethod(_parent, "getString", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(QString, text)); return ret; } QJSValue ScriptEngine::getInt(const QString &label, int value) { QJSValue ret; QMetaObject::invokeMethod(_parent, "getInt", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(int, value)); return ret; } QJSValue ScriptEngine::getFloat(const QString &label, double value, int decimals) const { QJSValue ret; QMetaObject::invokeMethod(_parent, "getFloat", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(double, value), Q_ARG(int, decimals)); return ret; } QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, int current) const { QJSValue ret; QMetaObject::invokeMethod(_parent, "getItem", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QStringList, items), Q_ARG(QString, label), Q_ARG(int, current)); return ret; } bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async) { QString path; QDir dir(_outputDir); QFileInfo info(outpath); QString suffix = info.suffix(); if(info.isAbsolute()) path = info.absolutePath(); else path = dir.absoluteFilePath(outpath); QString f = format.toLower(); if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg" && f != "tiff") { logError("Output format must be one of xisf fits jpg png bmp tiff"); return false; } info.setFile(path); outpath = info.absolutePath() + "/" + info.completeBaseName() + "." + f; if(async) { _semaphore.acquire(); _pool->start(new ConvertRunable(file->absoluteFilePath(), outpath, f, params, &_semaphore)); } else { ConvertRunable crun(file->absoluteFilePath(), outpath, f, params, nullptr); crun.run(); } return true; } #ifdef PLATESOLVER void ScriptEngine::setStartingSolution(const QJSValue &solution) { if(solution.isObject()) { if(solution.hasProperty("ra") && solution.hasProperty("dec") && solution.property("ra").isNumber() && solution.property("dec").isNumber()) _solver->setSearchPosition(solution.property("ra").toNumber(), solution.property("dec").toNumber()); if(solution.hasProperty("pixscale") && solution.property("pixscale").isNumber()) { double scale = solution.property("pixscale").toNumber(); _solver->setSearchScale(scale * 0.8, scale * 1.2, SSolver::ScaleUnits::ARCSEC_PER_PIX); } } else { _solver->clearStartingPositionAndScale(); } } QJSValue ScriptEngine::solveImage(File *file, bool updateHeader) { QString path = file->absoluteFilePath(); QJSValue ret = newObject(); if(_solver->loadImage(path)) { if(_solver->solveImage(true)) { auto solution = _solver->getSolution(); ret.setProperty("fieldWidth", solution.fieldWidth); ret.setProperty("fieldHeight", solution.fieldHeight); ret.setProperty("ra", solution.ra); ret.setProperty("dec", solution.dec); ret.setProperty("orientation", solution.orientation); ret.setProperty("pixscale", solution.pixscale); ret.setProperty("parity", solution.parity == FITSImage::Parity::POSITIVE); ret.setProperty("raError", solution.raError); ret.setProperty("decError", solution.decError); if(updateHeader) { QString error; if(!_solver->updateHeader(error)) logError(error); } } else { logError("Failed to plate solve image " + path); } } else { logError("Failed to load image " + path); } return ret; } QJSValue ScriptEngine::extractStars(File *file, bool hfr) { QJSValue ret; QString path = file->absoluteFilePath(); if(_solver->loadImage(path)) { if(_solver->extractSources(hfr, true)) { auto stars = _solver->getStars(); ret = newArray(stars.size()); int i = 0; for(auto &star : stars) { QJSValue starj = newObject(); starj.setProperty("x", star.x); starj.setProperty("y", star.y); starj.setProperty("mag", star.mag); starj.setProperty("flux", star.flux); starj.setProperty("peak", star.peak); starj.setProperty("HFR", star.HFR); starj.setProperty("a", star.a); starj.setProperty("b", star.b); starj.setProperty("theta", star.theta); starj.setProperty("ra", star.ra); starj.setProperty("dec", star.dec); starj.setProperty("numPixels", star.numPixels); ret.setProperty(i++, starj); } } else { logError("Failed to extract sources from " + path); } } else { logError("Failed to load image " + path); } return ret; } #endif // PLATESOLVER QJSValue ScriptEngine::newObject() { return _jsEngine->newObject(); } QJSValue ScriptEngine::newArray(uint size) { return _jsEngine->newArray(size); } void ScriptEngine::run() { QJSValue jsPaths = _jsEngine->newArray(_paths.size()); for(qsizetype i=0; i<_paths.size(); i++) jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i].first, _paths[i].second, this))); _jsEngine->globalObject().setProperty("files", jsPaths); QFile scriptFile(_scriptPath); if(!scriptFile.open(QIODevice::ReadOnly)) { emit newMessage("Failed to open " + _scriptPath, true); emit finished(); return; } QTextStream stream(&scriptFile); QString contents = stream.readAll(); scriptFile.close(); QJSValue result = _jsEngine->evaluate(contents, _scriptPath); qDebug() << result.isError() << result.toString(); _pool->waitForDone(); if(result.isError()) { QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString(); error += "\n" + result.property("stack").toString(); emit newMessage(error, true); } emit finished(); } void File::loadFitsKeywords() { if(!_fitsKeywordsLoaded) { _fitsKeywordsLoaded = true; ImageInfoData info; if(suffix().toLower() == "xisf") { readXISFHeader(_path, info); } else if(suffix().toLower() == "fits" || suffix().toLower() == "fit") { readFITSHeader(_path, info); } else return; for(auto &record : info.fitsHeader) { _fitsKeywords.append(record.key); _fitsRecords.insert(record.key, record); } } } bool File::mkpath(const QString &path) const { QFileInfo info(path); if(!info.isRelative()) { _engine->logError("Destination path is not relative"); return false; } QDir dir(_engine->outputDir()); if(dir.mkpath(info.path())) { return true; } else { _engine->logError("Failed to create dir " + info.path()); return false; } } File::File(const QString &path, Script::ScriptEngine *engine) : File(path, QString(), engine) { } File::File(const QString &path, const QString &root, ScriptEngine *engine) : _engine(engine), _path(path), _root(root), _info(path) { } QString File::fileName() const { return _info.fileName(); } QString File::absoluteFilePath() const { return _info.absoluteFilePath(); } QString File::absolutePath() const { return _info.absolutePath(); } QString File::relativeFilePath() const { QDir dir(_root); return dir.relativeFilePath(_info.absoluteFilePath()); } QString File::relativePath() const { QDir dir(_root); return dir.relativeFilePath(_info.absolutePath()); } QString File::baseName() const { return _info.baseName(); } QString File::completeBaseName() const { return _info.completeBaseName(); } QString File::suffix() const { return _info.suffix(); } qint64 File::size() const { return _info.size(); } QStringList File::fitsKeywords() { loadFitsKeywords(); return _fitsKeywords; } QString File::fitsValue(const QString &key) { loadFitsKeywords(); if(_fitsRecords.contains(key)) return _fitsRecords[key].value.toString(); else return QString(); } QJSValue File::fitsValues(const QString &key) { loadFitsKeywords(); if(_fitsRecords.contains(key)) { QList values = _fitsRecords.values(key); QJSValue array = _engine->newArray(values.size()); for(qsizetype i=0; inewArray(_fitsRecords.size()); uint i = 0; for(auto &record : _fitsRecords) { QJSValue item = _engine->newObject(); item.setProperty("key", QString::fromUtf8(record.key)); item.setProperty("value", record.value.toString()); item.setProperty("comment", QString::fromUtf8(record.comment)); item.setProperty("xisf", record.xisf); array.setProperty(i++, item); } return array; } bool File::modifyFITSRecords(const FITSRecordModify *modify) { _fitsKeywordsLoaded = false; _fitsKeywords.clear(); if(QRegularExpression("(fits?|fz|fts)", QRegularExpression::CaseInsensitiveOption).match(suffix()).hasMatch()) { fitsfile *file; int status = 0; fits_open_diskfile(&file, _path.toLocal8Bit().data(), READWRITE, &status); int num = 0; fits_get_num_hdus(file, &num, &status); if(status) { if(_engine)_engine->newMessage("Failed to open FITS file", true); return false; } int imgtype; int naxis; long naxes[3] = {0}; int type = -1; for(int i=1; i <= num; i++) { fits_movabs_hdu(file, i, IMAGE_HDU, &status); fits_get_hdu_type(file, &type, &status); fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status); if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0) break; if(i == num)return false; } for(auto &remove : modify->_remove) { int status = 0;//we ignore errors from here fits_delete_key(file, remove.toLatin1().data(), &status); } for(auto &record : modify->_update) { switch(record.value.typeId()) { case QMetaType::Bool: { int val = record.value.toBool(); fits_update_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::Int: case QMetaType::UInt: { long long val = record.value.toLongLong(); fits_update_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::QString: { QByteArray val = record.value.toString().toLatin1(); fits_update_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::Float: case QMetaType::Double: { double val = record.value.toDouble(); fits_update_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } default: if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true); return false; break; } if(status) { char error[100]; fits_get_errstatus(status, error); if(_engine)_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true); return false; } } for(auto &record : modify->_add) { switch(record.value.typeId()) { case QMetaType::Bool: { int val = record.value.toBool(); fits_write_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::Int: case QMetaType::UInt: { long long val = record.value.toLongLong(); fits_write_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::QString: { QByteArray val = record.value.toString().toLatin1(); fits_write_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } case QMetaType::Float: case QMetaType::Double: { double val = record.value.toDouble(); fits_write_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); break; } default: if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true); return false; break; } if(status) { char error[100]; fits_get_errstatus(status, error); if(_engine)_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true); return false; } } fits_close_file(file, &status); return status == 0; } else if(suffix().toLower() == "xisf") { try { LibXISF::XISFModify modifyXISF; modifyXISF.open(_path.toLocal8Bit().data()); QFileInfo in(_path); QFileInfo out(_path + "~"); for(auto &remove : modify->_remove) modifyXISF.removeFITSKeyword(0, remove.toStdString()); for(auto &record : modify->_update) modifyXISF.updateFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()}, true); for(auto &record : modify->_add) modifyXISF.addFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()}); modifyXISF.save(out.absoluteFilePath().toLocal8Bit().toStdString()); modifyXISF.close(); std::filesystem::rename(out.filesystemAbsoluteFilePath(), in.filesystemAbsoluteFilePath()); return true; } catch(std::filesystem::filesystem_error &err) { return false; } catch(LibXISF::Error &err) { if(_engine)_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true); return false; } } return false; } bool File::isMarked() const { return _engine->isMarked(this); } File* File::copy(const QString &newpath) const { if(mkpath(newpath)) { if(QFile::copy(_path, _engine->outputDir() + newpath)) return new File(_engine->outputDir() + newpath, _engine); _engine->logError("Failed copy to " + newpath); return nullptr; } return nullptr; } bool File::move(const QString &newpath) { if(mkpath(newpath)) { if(QFile::rename(_path, _engine->outputDir() + newpath)) { _path = _engine->outputDir() + newpath; return true; } _engine->logError("Failed move to " + newpath); return false; } return false; } File* File::convert(const QString &outpath, const QString &format, const QVariantMap ¶ms) { QString path = outpath; if(_engine->convert(this, path, format, params, false)) return new File(path, _engine); else return nullptr; } File* File::convertAsync(const QString &outpath, const QString &format, const QVariantMap ¶ms) { QString path = outpath; if(_engine->convert(this, path, format, params, true)) return new File(path, _engine); else return nullptr; } QJSValue File::stats() { if(_stats.isUndefined()) { ImageInfoData info; std::shared_ptr rawImage; loadImage(_path, info, rawImage); rawImage->calcStats(); RawImage::Stats stats = rawImage->imageStats(); _stats = _engine->newObject(); _stats.setProperty("mean", stats.m_mean[0]); _stats.setProperty("stddev", stats.m_stdDev[0]); _stats.setProperty("median", stats.m_median[0]); _stats.setProperty("min", stats.m_min[0]); _stats.setProperty("max", stats.m_max[0]); _stats.setProperty("mad", stats.m_mean[0]); } return _stats; } #ifdef PLATESOLVER QJSValue File::solve(bool updateHeader) { if(_solution.isUndefined() || updateHeader) _solution = _engine->solveImage(this, updateHeader); return _solution; } QJSValue File::extractStars(bool hfr) { if(_stars.isUndefined()) _stars = _engine->extractStars(this, hfr); return _stars; } #endif // PLATESOLVER ScriptEngineThread::ScriptEngineThread(BatchProcessing *parent) : QObject(parent) { _thread = new QThread(); _thread->setObjectName("ScriptEngine"); _engine = new ScriptEngine(parent); _engine->moveToThread(_thread); connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit); connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage); connect(_thread, &QThread::started, _engine, &ScriptEngine::run); connect(_thread, &QThread::finished, _engine, &ScriptEngine::deleteLater); connect(_engine, &ScriptEngine::destroyed, [this](){ _engine = nullptr; }); connect(_thread, &QThread::finished, _thread, &QThread::deleteLater); connect(_thread, &QThread::finished, this, &ScriptEngineThread::finished); } ScriptEngineThread::~ScriptEngineThread() { if(_engine)_engine->interrupt(); } void ScriptEngineThread::setParams(const QString &scriptPath, const QList> &paths, const QString &outputDir) { _engine->setParams(scriptPath, paths, outputDir); } void ScriptEngineThread::start() { _thread->start(); } void ScriptEngineThread::interrupt() { if(_engine)_engine->interrupt(); } void FITSRecordModify::removeKeyword(const QString &key) { if(!_remove.contains(key)) _remove.append(key); } void FITSRecordModify::updateKeyword(const QString &key, const QVariant &value, const QString &comment) { _update.append({key.toLatin1(), value, comment.toLatin1()}); } void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, const QString &comment) { _update.append({key.toLatin1(), value, comment.toLatin1()}); } }