Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6bc792ff7 | |||
| 1a307d82f9 | |||
| 8c5e2b2ebf | |||
| 03ad135ef0 | |||
| 2a78a9a41d | |||
| 1a214a169e | |||
| f8704c51d8 | |||
| 3feee0256c | |||
| 53472d807c | |||
| 9f06269aa4 | |||
| 78f242d808 | |||
| e6bab45a89 | |||
| 58286d52c5 | |||
| bac1963fa4 | |||
| 2415717ce0 | |||
| e7acbca01e | |||
| 7c4118b0b6 | |||
| 8178efdafd | |||
| 90026f931f | |||
| eee4613b25 | |||
| 24a9e96bbf | |||
| 5af5f4f068 | |||
| 85f9822b96 | |||
| 7fc6c64fd7 | |||
| 4488c2e6af | |||
| 0047607c1d | |||
| 45c368bbbb | |||
| c96cb86a29 |
+4
-2
@@ -17,7 +17,7 @@ if(SANITIZE_ADDRESS_LEAK)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak")
|
||||
endif(SANITIZE_ADDRESS_LEAK)
|
||||
|
||||
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml Charts REQUIRED)
|
||||
find_library(EXIF_LIB exif REQUIRED)
|
||||
find_library(FITS_LIB cfitsio REQUIRED)
|
||||
find_library(RAW_LIB NAMES raw_r REQUIRED)
|
||||
@@ -30,6 +30,7 @@ add_subdirectory(libXISF)
|
||||
set(TENMON_SRC
|
||||
about.cpp about.h
|
||||
batchprocessing.cpp batchprocessing.h batchprocessing.ui
|
||||
chartgraph.h chartgraph.cpp
|
||||
database.cpp database.h
|
||||
databaseview.cpp databaseview.h
|
||||
delete.cpp
|
||||
@@ -102,7 +103,7 @@ if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
|
||||
message(STATUS "Found stellarsolver ${STELLARSOLVER_INCLUDE} ${STELLARSOLVER_LIB}")
|
||||
endif(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
|
||||
|
||||
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF)
|
||||
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml Qt6::Charts ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF)
|
||||
if(APPLE)
|
||||
target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation")
|
||||
elseif(UNIX)
|
||||
@@ -129,6 +130,7 @@ if(UNIX AND NOT APPLE)
|
||||
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
||||
install(FILES resources/space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
|
||||
install(FILES resources/space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
|
||||
install(FILES space.nouspiro.tenmon.xisf.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/mime/packages")
|
||||
endif()
|
||||
install(FILES space.nouspiro.tenmon.metainfo.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
|
||||
endif(UNIX AND NOT APPLE)
|
||||
|
||||
@@ -32,6 +32,8 @@ HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Help"));
|
||||
resize(1000, 600);
|
||||
setModal(false);
|
||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
QTextEdit *helpText = new QTextEdit(this);
|
||||
|
||||
@@ -184,6 +184,48 @@ Location of this directory is on Windows: "C:/Users/<USER>/AppData/Roaming
|
||||
core.log(JSON.stringify(profile));</pre></li>
|
||||
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
|
||||
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
|
||||
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
|
||||
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
|
||||
It return button that was clicked as a string.
|
||||
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
|
||||
</li>
|
||||
<li><b>plot(graph)</b> this method show graph defined by JS object.
|
||||
|
||||
<pre>
|
||||
var chart = {
|
||||
"title": "Chart title", // Title that will show on top of chart
|
||||
"legend":
|
||||
{
|
||||
"visible": true,// default is true
|
||||
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
|
||||
},
|
||||
"series":[ // array of data series
|
||||
{
|
||||
"title": "HFR",
|
||||
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
|
||||
"y":[2.5,3.1,2.6,2.2] // array of values
|
||||
},
|
||||
{
|
||||
"title": "Ecc",
|
||||
"y":[0.37, 0.4, 0.35, 0.25],
|
||||
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
|
||||
},
|
||||
{
|
||||
"title": "Stars",
|
||||
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
|
||||
"shape": "star", // shape of markers. valid only for points
|
||||
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
|
||||
"y":[523,412,487,510],
|
||||
"y2": true, // if set to true this serie will use secondary Y axis
|
||||
"bestFit": true, // show best fit line
|
||||
"color": "#0000ff"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
core.plot(chart);
|
||||
</pre>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
|
||||
@@ -135,6 +135,54 @@ Le deuxième paramètre est la valeur par défaut dans la zone de saisie. Les tr
|
||||
<li><b>getItem(items)</b> affiche une boîte de dialogue de sélection qui permet de sélectionner un élément dans un tableau d'éléments. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
|
||||
<li><b>setStartingSolution(solution)</b> with this you can set starting point and image scale. It accepth object with attributes "ra", "dec", "pixscale".
|
||||
Same object as returned by <i>File.solve()</i> method. You can also call it without paramer in which case it will clear any previously set values.</li>
|
||||
<li><b>getSolverProfile()</b> return solver profile as Object.
|
||||
<pre>var profile = core.getSolverProfile();
|
||||
core.log(JSON.stringify(profile));</pre></li>
|
||||
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
|
||||
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
|
||||
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
|
||||
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
|
||||
It return button that was clicked as a string.
|
||||
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
|
||||
</li>
|
||||
<li><b>plot(graph)</b> this method show graph defined by JS object.
|
||||
|
||||
<pre>
|
||||
var chart = {
|
||||
"title": "Chart title", // Title that will show on top of chart
|
||||
"legend":
|
||||
{
|
||||
"visible": true,// default is true
|
||||
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
|
||||
},
|
||||
"series":[ // array of data series
|
||||
{
|
||||
"title": "HFR",
|
||||
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
|
||||
"y":[2.5,3.1,2.6,2.2] // array of values
|
||||
},
|
||||
{
|
||||
"title": "Ecc",
|
||||
"y":[0.37, 0.4, 0.35, 0.25],
|
||||
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
|
||||
},
|
||||
{
|
||||
"title": "Stars",
|
||||
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
|
||||
"shape": "star", // shape of markers. valid only for points
|
||||
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
|
||||
"y":[523,412,487,510],
|
||||
"y2": true, // if set to true this serie will use secondary Y axis
|
||||
"bestFit": true, // show best fit line
|
||||
"color": "#0000ff"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
core.plot(chart);
|
||||
</pre>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
|
||||
@@ -126,6 +126,54 @@ V skripte je dostupný globálny objekt nazvaný <b>core</b> ktorý má nasledov
|
||||
<li><b>getItem(items)</b> ukáže dialog pre výber jednej hodnoty z poľa hodnôt. Vracia vybranú hodnotu ako String alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
|
||||
<li><b>setStartingSolution(solution)</b> with this you can set starting point and image scale. It accepth object with attributes "ra", "dec", "pixscale".
|
||||
Same object as returned by <i>File.solve()</i> method. You can also call it without paramer in which case it will clear any previously set values.</li>
|
||||
<li><b>getSolverProfile()</b> return solver profile as Object.
|
||||
<pre>var profile = core.getSolverProfile();
|
||||
core.log(JSON.stringify(profile));</pre></li>
|
||||
<li><b>setSolverProfile(index)</b> set solver profile by index. Valid values are from 1 to 8 as in GUI.</li>
|
||||
<li><b>setSolverProfile(profile)</b> set solver profile. Parameter is same as object returned by <i>getSolverProfile()</i> method</li>
|
||||
<li><b>question(question, buttons, title = "")</b> show dialog with question. First argument <i>question</i> is string. Second argument <i>buttons</i>
|
||||
is array of following strings. "ok", "yes", "no", "yesall", "noall", "abort", "retry", "ignore", "cancel", "discard", "apply", "reset" Third argument <i>title</i> is optional string that show in title bar.
|
||||
It return button that was clicked as a string.
|
||||
<pre>var button = core.question("Apply to all files?", ["yes", "no"]);</pre>
|
||||
</li>
|
||||
<li><b>plot(graph)</b> this method show graph defined by JS object.
|
||||
|
||||
<pre>
|
||||
var chart = {
|
||||
"title": "Chart title", // Title that will show on top of chart
|
||||
"legend":
|
||||
{
|
||||
"visible": true,// default is true
|
||||
"align": "left" // allowed values are "top", "right", "bottom", "left". default is "top"
|
||||
},
|
||||
"series":[ // array of data series
|
||||
{
|
||||
"title": "HFR",
|
||||
"type": "bar", // type of the serie. Can be one of "line", "points", "linePoints", "bar"
|
||||
"y":[2.5,3.1,2.6,2.2] // array of values
|
||||
},
|
||||
{
|
||||
"title": "Ecc",
|
||||
"y":[0.37, 0.4, 0.35, 0.25],
|
||||
"color": "red" // color of serie. It can be name of color like "green" or RGB hex value "#ccddff"
|
||||
},
|
||||
{
|
||||
"title": "Stars",
|
||||
"type": "points", // type of serie. can be "line", "points", "linePoints" and "bar". Default is "line"
|
||||
"shape": "star", // shape of markers. valid only for points
|
||||
"x":[1, 2.5, 3.5, 6],// line, points and linePoints can also have "x" values. Otherwise it assume sequence 0,1,2 ...
|
||||
"y":[523,412,487,510],
|
||||
"y2": true, // if set to true this serie will use secondary Y axis
|
||||
"bestFit": true, // show best fit line
|
||||
"color": "#0000ff"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
core.plot(chart);
|
||||
</pre>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
|
||||
+67
-13
@@ -10,7 +10,11 @@
|
||||
#include <QMessageBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QInputDialog>
|
||||
#include <QChart>
|
||||
#include <QChartView>
|
||||
#include <QLineSeries>
|
||||
#include "scriptengine.h"
|
||||
#include "chartgraph.h"
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QCloseEvent>
|
||||
@@ -180,19 +184,7 @@ void BatchProcessing::browse()
|
||||
|
||||
void BatchProcessing::openScriptDir()
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
QDBusConnection con = QDBusConnection::sessionBus();
|
||||
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
|
||||
QList<QVariant> args = {QStringList(QUrl::fromLocalFile(_scriptBasePath).toString()), QString()};
|
||||
message.setArguments(args);
|
||||
con.call(message);
|
||||
#endif
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(_scriptBasePath)});
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(_scriptBasePath));
|
||||
#endif
|
||||
openDir(_scriptBasePath);
|
||||
}
|
||||
|
||||
void BatchProcessing::runScript()
|
||||
@@ -279,3 +271,65 @@ QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label
|
||||
QString ret = QInputDialog::getItem(this, tr("Select item"), label, items, current, false, &ok);
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
|
||||
QJSValue BatchProcessing::question(const QString &question, const QStringList &buttons, const QString &title)
|
||||
{
|
||||
QMessageBox::StandardButtons standardButtons = QMessageBox::NoButton;
|
||||
if(buttons.contains("ok"))standardButtons |= QMessageBox::Ok;
|
||||
if(buttons.contains("yes"))standardButtons |= QMessageBox::Yes;
|
||||
if(buttons.contains("no"))standardButtons |= QMessageBox::No;
|
||||
if(buttons.contains("yesall"))standardButtons |= QMessageBox::YesToAll;
|
||||
if(buttons.contains("noall"))standardButtons |= QMessageBox::NoToAll;
|
||||
if(buttons.contains("abort"))standardButtons |= QMessageBox::Abort;
|
||||
if(buttons.contains("retry"))standardButtons |= QMessageBox::Retry;
|
||||
if(buttons.contains("ignore"))standardButtons |= QMessageBox::Ignore;
|
||||
if(buttons.contains("cancel"))standardButtons |= QMessageBox::Cancel;
|
||||
if(buttons.contains("discard"))standardButtons |= QMessageBox::Discard;
|
||||
if(buttons.contains("apply"))standardButtons |= QMessageBox::Apply;
|
||||
if(buttons.contains("reset"))standardButtons |= QMessageBox::Reset;
|
||||
if(standardButtons == QMessageBox::NoButton)standardButtons = QMessageBox::Ok;
|
||||
|
||||
QMessageBox::StandardButton button = QMessageBox::question(this, title, question, standardButtons);
|
||||
QJSValue ret;
|
||||
switch(button)
|
||||
{
|
||||
default:
|
||||
case QMessageBox::Ok: ret = "ok"; break;
|
||||
case QMessageBox::Yes: ret = "yes"; break;
|
||||
case QMessageBox::No: ret = "no"; break;
|
||||
case QMessageBox::YesToAll: ret = "yesall"; break;
|
||||
case QMessageBox::NoToAll: ret = "noall"; break;
|
||||
case QMessageBox::Abort: ret = "abort"; break;
|
||||
case QMessageBox::Retry: ret = "retry"; break;
|
||||
case QMessageBox::Ignore: ret = "ignore"; break;
|
||||
case QMessageBox::Cancel: ret = "cancel"; break;
|
||||
case QMessageBox::Discard: ret = "discard"; break;
|
||||
case QMessageBox::Apply: ret = "apply"; break;
|
||||
case QMessageBox::Reset: ret = "reset"; break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void BatchProcessing::plot(const QVariant &graph)
|
||||
{
|
||||
ChartGraph *chart = new ChartGraph(this);
|
||||
chart->plot(graph);
|
||||
}
|
||||
|
||||
void openDir(const QString &path)
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
QDBusConnection con = QDBusConnection::sessionBus();
|
||||
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
|
||||
QList<QVariant> args = {QStringList(QUrl::fromLocalFile(path).toString()), QString()};
|
||||
message.setArguments(args);
|
||||
con.call(message);
|
||||
#endif
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(path)});
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ public slots:
|
||||
QJSValue getInt(const QString &label, int value);
|
||||
QJSValue getFloat(const QString &label, double value, int decimals);
|
||||
QJSValue getItem(const QStringList &items, const QString &label, int current);
|
||||
QJSValue question(const QString &question, const QStringList &buttons, const QString &title = "");
|
||||
|
||||
void plot(const QVariant &graph);
|
||||
};
|
||||
|
||||
void openDir(const QString &path);
|
||||
|
||||
#endif // BATCHPROCESSING_H
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
#include "chartgraph.h"
|
||||
#include <QChartView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLineSeries>
|
||||
#include <QBarSeries>
|
||||
#include <QBarSet>
|
||||
#include <QBarCategoryAxis>
|
||||
#include <QScatterSeries>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QValueAxis>
|
||||
#include <QFileDialog>
|
||||
#include <QSettings>
|
||||
#include <QToolBar>
|
||||
#include <QStyle>
|
||||
|
||||
class ChartView : public QChartView
|
||||
{
|
||||
QPointF _mousePos;
|
||||
bool _scroll = false;
|
||||
public:
|
||||
ChartView(QWidget *parent) : QChartView(parent)
|
||||
{
|
||||
}
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override
|
||||
{
|
||||
if(!chart()->isZoomed())chart()->zoom(0.999999);//workaround so zoomReset() reset scroll
|
||||
switch(event->key())
|
||||
{
|
||||
case Qt::Key_Plus:
|
||||
chart()->zoomIn();
|
||||
break;
|
||||
case Qt::Key_Minus:
|
||||
chart()->zoomOut();
|
||||
break;
|
||||
case Qt::Key_Left:
|
||||
chart()->scroll(-10, 0);
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
chart()->scroll(10, 0);
|
||||
break;
|
||||
case Qt::Key_Up:
|
||||
chart()->scroll(0, 10);
|
||||
break;
|
||||
case Qt::Key_Down:
|
||||
chart()->scroll(0, -10);
|
||||
break;
|
||||
default:
|
||||
QGraphicsView::keyPressEvent(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
void mousePressEvent(QMouseEvent *event) override
|
||||
{
|
||||
if(event->button() == Qt::LeftButton)
|
||||
{
|
||||
_scroll = true;
|
||||
if(!chart()->isZoomed())chart()->zoom(0.999999);//workaround so zoomReset() reset scroll
|
||||
_mousePos = event->position();
|
||||
}
|
||||
|
||||
QChartView::mousePressEvent(event);
|
||||
}
|
||||
void mouseMoveEvent(QMouseEvent *event) override
|
||||
{
|
||||
if(_scroll)
|
||||
{
|
||||
QPointF pos = event->position();
|
||||
chart()->scroll(_mousePos.x() - pos.x(), pos.y() - _mousePos.y());
|
||||
_mousePos = pos;
|
||||
}
|
||||
QChartView::mouseMoveEvent(event);
|
||||
}
|
||||
void mouseReleaseEvent(QMouseEvent *event) override
|
||||
{
|
||||
_scroll = false;
|
||||
QChartView::mouseReleaseEvent(event);
|
||||
}
|
||||
void wheelEvent(QWheelEvent *event) override
|
||||
{
|
||||
if(event->angleDelta().y() > 0)
|
||||
chart()->zoomIn();
|
||||
if(event->angleDelta().y() < 0)
|
||||
chart()->zoomOut();
|
||||
}
|
||||
};
|
||||
|
||||
ChartGraph::ChartGraph(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(tr("Chart"));
|
||||
|
||||
_chartView = new ChartView(this);
|
||||
setCentralWidget(_chartView);
|
||||
|
||||
_chart = new QChart;
|
||||
_chartView->setChart(_chart);
|
||||
_chartView->setRenderHint(QPainter::Antialiasing);
|
||||
resize(1024, 768);
|
||||
|
||||
menuBar()->addAction(tr("Save"), this, &ChartGraph::save);
|
||||
menuBar()->addAction(tr("Reset view"), [this](){ _chart->zoomReset(); });
|
||||
}
|
||||
|
||||
void ChartGraph::plot(const QVariant &graph)
|
||||
{
|
||||
QVariantMap map = graph.toMap();
|
||||
|
||||
_chart->setTitle(map["title"].toString());
|
||||
if(map.contains("legend"))
|
||||
{
|
||||
QVariantMap legend = map["legend"].toMap();
|
||||
if(legend.contains("visible"))
|
||||
_chart->legend()->setVisible(legend["visible"].toBool());
|
||||
|
||||
QString align = legend["align"].toString();
|
||||
if(align == "top")
|
||||
_chart->legend()->setAlignment(Qt::AlignTop);
|
||||
else if(align == "left")
|
||||
_chart->legend()->setAlignment(Qt::AlignLeft);
|
||||
else if(align == "bottom")
|
||||
_chart->legend()->setAlignment(Qt::AlignBottom);
|
||||
else if(align == "right")
|
||||
_chart->legend()->setAlignment(Qt::AlignRight);
|
||||
}
|
||||
|
||||
QBarSeries *barSeries = nullptr;
|
||||
|
||||
qreal minX = INFINITY;
|
||||
qreal maxX = -INFINITY;
|
||||
qreal minY = INFINITY;
|
||||
qreal maxY = -INFINITY;
|
||||
qreal minY2 = INFINITY;
|
||||
qreal maxY2 = -INFINITY;
|
||||
|
||||
QValueAxis *xaxis = new QValueAxis(_chart);
|
||||
QBarCategoryAxis *barxaxis = new QBarCategoryAxis(_chart);
|
||||
QValueAxis *yaxis = new QValueAxis(_chart);
|
||||
QValueAxis *y2axis = new QValueAxis(_chart);
|
||||
_chart->addAxis(xaxis, Qt::AlignBottom);
|
||||
_chart->addAxis(yaxis, Qt::AlignLeft);
|
||||
_chart->addAxis(y2axis, Qt::AlignRight);
|
||||
_chart->addAxis(barxaxis, Qt::AlignBottom);
|
||||
y2axis->setGridLinePen(Qt::DashDotLine);
|
||||
|
||||
for(auto s : map["series"].toList())
|
||||
{
|
||||
QVariantMap serie = s.toMap();
|
||||
QString type = serie["type"].toString();
|
||||
bool y2 = serie["y2"].toBool();
|
||||
|
||||
if(type == "line" || type == "points" || type == "linePoints" || type.isEmpty())
|
||||
{
|
||||
QXYSeries *series = nullptr;
|
||||
if(type == "points")
|
||||
{
|
||||
QScatterSeries *scatter = new QScatterSeries(_chart);
|
||||
series = scatter;
|
||||
QString shape = serie["shape"].toString();
|
||||
if(shape == "circle")scatter->setMarkerShape(QScatterSeries::MarkerShapeCircle);
|
||||
else if(shape == "rectangle")scatter->setMarkerShape(QScatterSeries::MarkerShapeRectangle);
|
||||
else if(shape == "triangle")scatter->setMarkerShape(QScatterSeries::MarkerShapeTriangle);
|
||||
else if(shape == "star")scatter->setMarkerShape(QScatterSeries::MarkerShapeStar);
|
||||
else if(shape == "pentagon")scatter->setMarkerShape(QScatterSeries::MarkerShapePentagon);
|
||||
}
|
||||
else
|
||||
{
|
||||
series = new QLineSeries(_chart);
|
||||
}
|
||||
|
||||
series->setName(serie["title"].toString());
|
||||
QVariantList x = serie["x"].toList();
|
||||
QVariantList y = serie["y"].toList();
|
||||
if(x.isEmpty())
|
||||
{
|
||||
for(int i = 0; i < y.size(); i++)
|
||||
{
|
||||
qreal val = y[i].toDouble();
|
||||
if(y2)
|
||||
{
|
||||
minY2 = std::min(minY2, val);
|
||||
maxY2 = std::max(maxY2, val);
|
||||
}
|
||||
else
|
||||
{
|
||||
minY = std::min(minY, val);
|
||||
maxY = std::max(maxY, val);
|
||||
}
|
||||
series->append(i, val);
|
||||
}
|
||||
minX = std::min(minX, 0.0);
|
||||
maxX = std::max(maxX, y.size() - 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
int size = std::min(x.size(), y.size());
|
||||
for(int i = 0; i < size; i++)
|
||||
{
|
||||
qreal val = y[i].toDouble();
|
||||
if(y2)
|
||||
{
|
||||
minY2 = std::min(minY2, val);
|
||||
maxY2 = std::max(maxY2, val);
|
||||
}
|
||||
else
|
||||
{
|
||||
minY = std::min(minY, val);
|
||||
maxY = std::max(maxY, val);
|
||||
}
|
||||
minX = std::min(minX, x[i].toDouble());
|
||||
maxX = std::max(maxX, x[i].toDouble());
|
||||
series->append(x[i].toDouble(), val);
|
||||
}
|
||||
}
|
||||
|
||||
_chart->addSeries(series);
|
||||
series->attachAxis(xaxis);
|
||||
series->attachAxis(y2 ? y2axis : yaxis);
|
||||
|
||||
if(serie.contains("color"))
|
||||
{
|
||||
QString color = serie["color"].toString();
|
||||
if(QColor::isValidColorName(color))series->setColor(QColor::fromString(color));
|
||||
}
|
||||
|
||||
if(serie["bestFit"].toBool())
|
||||
{
|
||||
series->setBestFitLineVisible(true);
|
||||
QPen pen = series->bestFitLinePen();
|
||||
pen.setColor(series->color());
|
||||
pen.setStyle(Qt::DashLine);
|
||||
series->setBestFitLinePen(pen);
|
||||
}
|
||||
|
||||
if(type == "linePoints")
|
||||
series->setPointsVisible(true);
|
||||
|
||||
}
|
||||
else if(type == "bar")
|
||||
{
|
||||
if(!barSeries)
|
||||
{
|
||||
barSeries = new QBarSeries(_chart);
|
||||
_chart->addSeries(barSeries);
|
||||
barSeries->attachAxis(yaxis);
|
||||
barSeries->attachAxis(barxaxis);
|
||||
}
|
||||
QBarSet *set = new QBarSet(serie["title"].toString());
|
||||
QVariantList y = serie["y"].toList();
|
||||
for(int i = 0; i < y.size(); i++)
|
||||
{
|
||||
qreal val = y[i].toDouble();
|
||||
minY = std::min(minY, val);
|
||||
maxY = std::max(maxY, val);
|
||||
set->append(val);
|
||||
}
|
||||
|
||||
barSeries->append(set);
|
||||
for(int i = barxaxis->count() + 1; i <= y.size(); i++)
|
||||
barxaxis->append(QString::number(i));
|
||||
|
||||
if(serie.contains("color"))
|
||||
{
|
||||
QString color = serie["color"].toString();
|
||||
if(QColor::isValidColorName(color))set->setColor(QColor::fromString(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(barSeries)
|
||||
{
|
||||
xaxis->setRange(std::min(minX, -0.5), std::max(maxX, barxaxis->count() - 0.5));
|
||||
minY = std::min(minY, 0.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
xaxis->setRange(minX, maxX);
|
||||
}
|
||||
|
||||
yaxis->setRange(minY, maxY);
|
||||
y2axis->setRange(minY2, maxY2);
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
void ChartGraph::save()
|
||||
{
|
||||
QSettings settings;
|
||||
QString dir = settings.value("mainwindow/lastdir").toString();
|
||||
QString output = QFileDialog::getSaveFileName(this, tr("Save as"), dir, "PNG (*.png)");
|
||||
|
||||
if(!output.isEmpty())
|
||||
{
|
||||
QPixmap graph = _chartView->grab();
|
||||
graph.toImage().save(output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef CHARTGRAPH_H
|
||||
#define CHARTGRAPH_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QJSValue>
|
||||
#include <QChart>
|
||||
|
||||
class ChartView;
|
||||
|
||||
class ChartGraph : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
QChart *_chart;
|
||||
ChartView *_chartView;
|
||||
public:
|
||||
explicit ChartGraph(QWidget *parent = nullptr);
|
||||
void plot(const QVariant &graph);
|
||||
public slots:
|
||||
void save();
|
||||
};
|
||||
|
||||
#endif // CHARTGRAPH_H
|
||||
+19
-2
@@ -10,6 +10,7 @@
|
||||
#include <QContextMenuEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <iostream>
|
||||
#include "batchprocessing.h"
|
||||
|
||||
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
|
||||
|
||||
@@ -214,6 +215,8 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
|
||||
QMenu menu;
|
||||
QAction *mark = menu.addAction(tr("Mark"));
|
||||
QAction *unmark = menu.addAction(tr("Unmark"));
|
||||
QAction *open = menu.addAction(tr("Open"));
|
||||
QAction *openDirAction = menu.addAction(tr("Open file location"));
|
||||
|
||||
QAction *a = menu.exec(event->globalPos());
|
||||
if(a == nullptr)
|
||||
@@ -225,7 +228,10 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
|
||||
emit filesMarked(indexes);
|
||||
else if(a == unmark)
|
||||
emit filesUnmarked(indexes);
|
||||
|
||||
else if(a == open)
|
||||
emit openFile(indexes);
|
||||
else if(a == openDirAction)
|
||||
emit openDir(indexes);
|
||||
}
|
||||
|
||||
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
|
||||
@@ -270,6 +276,17 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
m_database->unmark(files);
|
||||
m_model->filesUnmarked(indexes);
|
||||
});
|
||||
connect(m_tableView, &DatabaseTableView::openFile, [this](QModelIndexList indexes){
|
||||
if(indexes.size())
|
||||
emit loadFile(m_model->data(indexes.front().siblingAtColumn(0)).toString());
|
||||
});
|
||||
connect(m_tableView, &DatabaseTableView::openDir, [this](QModelIndexList indexes){
|
||||
if(indexes.size())
|
||||
{
|
||||
QFileInfo info(m_model->data(indexes.front().siblingAtColumn(0)).toString());
|
||||
openDir(info.absolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
auto addFilterItems = [](QComboBox *combobox, const QStringList &fitsKeywords)
|
||||
{
|
||||
@@ -308,7 +325,7 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
}
|
||||
|
||||
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
|
||||
connect(filterButton, SIGNAL(pressed()), this, SLOT(applyFilter()));
|
||||
connect(filterButton, &QPushButton::pressed, this, &DataBaseView::applyFilter);
|
||||
hlayout->addWidget(filterButton);
|
||||
|
||||
connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
|
||||
|
||||
@@ -52,6 +52,8 @@ protected:
|
||||
signals:
|
||||
void filesMarked(QModelIndexList indexes);
|
||||
void filesUnmarked(QModelIndexList indexes);
|
||||
void openFile(QModelIndexList indexes);
|
||||
void openDir(QModelIndexList indexes);
|
||||
};
|
||||
|
||||
class DataBaseView : public QWidget
|
||||
|
||||
+10
-1
@@ -2,6 +2,8 @@
|
||||
#include <QSettings>
|
||||
#include <QHeaderView>
|
||||
|
||||
QMap<QString, QColor> headerHighlight;
|
||||
|
||||
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
|
||||
{
|
||||
setColumnCount(3);
|
||||
@@ -25,7 +27,14 @@ void ImageInfo::setInfo(const ImageInfoData &info)
|
||||
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
|
||||
for(const FITSRecord &record : info.fitsHeader)
|
||||
{
|
||||
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
|
||||
if(headerHighlight.contains(record.key))
|
||||
{
|
||||
QColor color = headerHighlight[record.key];
|
||||
item->setBackground(0, color);
|
||||
item->setBackground(1, color);
|
||||
item->setBackground(2, color);
|
||||
}
|
||||
}
|
||||
addTopLevelItem(fitsHeader);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ struct ImageInfoData
|
||||
QVector<QPair<QString, QString>> info;
|
||||
std::shared_ptr<WCSDataT> wcs;
|
||||
SkyPointScale getCenterRaDec() const;
|
||||
int index = 0;
|
||||
int num = 1;
|
||||
};
|
||||
|
||||
typedef enum
|
||||
|
||||
+65
-18
@@ -23,13 +23,16 @@ Image::Image(const QString name, int number, ImageRingList *ringList) :
|
||||
{
|
||||
}
|
||||
|
||||
void Image::load(QThreadPool *pool)
|
||||
void Image::load(int index, QThreadPool *pool)
|
||||
{
|
||||
if(index != m_info.index && !m_loading)
|
||||
m_rawImage.reset();
|
||||
|
||||
if(!m_rawImage && !m_loading)
|
||||
{
|
||||
m_loading = true;
|
||||
m_released = false;
|
||||
pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel()));
|
||||
pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel(), index));
|
||||
}
|
||||
if(!m_loading && m_rawImage)
|
||||
emit pixmapLoaded(this);
|
||||
@@ -38,7 +41,7 @@ void Image::load(QThreadPool *pool)
|
||||
void Image::loadThumbnail(QThreadPool *pool)
|
||||
{
|
||||
if(!m_thumbnail)
|
||||
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, true));
|
||||
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, 0, true));
|
||||
else
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
@@ -115,7 +118,7 @@ ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter,
|
||||
, m_nameFilter(nameFilter)
|
||||
, m_fileSuffix(nameFilter)
|
||||
{
|
||||
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
|
||||
connect(&m_fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &ImageRingList::dirChanged);
|
||||
m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
|
||||
m_loadPool = new QThreadPool(this);
|
||||
m_loadPool->setThreadPriority(QThread::LowPriority);
|
||||
@@ -146,6 +149,7 @@ bool ImageRingList::setDir(const QString path, const QString ¤tFile, bool
|
||||
|
||||
if(dir.exists())
|
||||
{
|
||||
m_currentDir = path;
|
||||
QStringList scannedDirs;
|
||||
QStringList absolutePaths;
|
||||
std::function<void(const QString&)> scanDir = [&](const QString &path)
|
||||
@@ -174,7 +178,8 @@ bool ImageRingList::setDir(const QString path, const QString ¤tFile, bool
|
||||
//qDebug() << absolutePaths.size();
|
||||
setFilesPrivate(absolutePaths, m_liveMode ? absolutePaths.first() : currentFile);
|
||||
|
||||
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
|
||||
if(m_fileSystemWatcher.directories().size())
|
||||
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
|
||||
m_fileSystemWatcher.addPath(path);
|
||||
return true;
|
||||
}
|
||||
@@ -212,6 +217,11 @@ ImagePtr ImageRingList::currentImage()
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString ImageRingList::currentDir() const
|
||||
{
|
||||
return m_currentDir;
|
||||
}
|
||||
|
||||
void ImageRingList::increment()
|
||||
{
|
||||
if(m_images.size())
|
||||
@@ -223,9 +233,9 @@ void ImageRingList::increment()
|
||||
(*m_firstImage)->release();
|
||||
m_firstImage = increment(m_firstImage);
|
||||
m_currImage = increment(m_currImage);
|
||||
(*m_currImage)->load(m_loadPool);
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load(m_loadPool);
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +250,37 @@ void ImageRingList::decrement()
|
||||
(*m_lastImage)->release();
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
m_currImage = decrement(m_currImage);
|
||||
(*m_currImage)->load(m_loadPool);
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
m_lastImage = decrement(m_lastImage);
|
||||
(*m_firstImage)->load(m_loadPool);
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::prevSubImage()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
int index = (*m_currImage)->info().index;
|
||||
int num = (*m_currImage)->info().num;
|
||||
if(num > 1)
|
||||
(*m_currImage)->load(index == 0 ? num - 1 : index - 1, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::nextSubImage()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
if((*m_currImage)->isLoading())
|
||||
return;
|
||||
|
||||
int index = (*m_currImage)->info().index;
|
||||
int num = (*m_currImage)->info().num;
|
||||
if(num > 1)
|
||||
(*m_currImage)->load((index + 1) % num, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +294,16 @@ void ImageRingList::setMarked()
|
||||
setFilesPrivate(files);
|
||||
}
|
||||
|
||||
void ImageRingList::reloadImage()
|
||||
{
|
||||
if(*m_currImage)
|
||||
{
|
||||
int index = (*m_currImage)->info().index;
|
||||
(*m_currImage)->release();
|
||||
(*m_currImage)->load(index, m_loadPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::setLiveMode(bool live)
|
||||
{
|
||||
m_liveMode = live;
|
||||
@@ -296,7 +344,7 @@ void ImageRingList::loadFile(int row)
|
||||
if(m_images.empty())
|
||||
return;
|
||||
|
||||
(*m_currImage)->load(m_loadPool);
|
||||
(*m_currImage)->load(0, m_loadPool);
|
||||
|
||||
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(m_liveMode)
|
||||
@@ -305,9 +353,9 @@ void ImageRingList::loadFile(int row)
|
||||
for(int i=0; i<m_width; i++)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load(m_loadPool);
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load(m_loadPool);
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
if(m_lastImage != m_firstImage)
|
||||
{
|
||||
@@ -431,9 +479,9 @@ void ImageRingList::setPreload(int width)
|
||||
for(int i = newWidth - m_width; i>0; i--)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load(m_loadPool);
|
||||
(*m_firstImage)->load(0, m_loadPool);
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load(m_loadPool);
|
||||
(*m_lastImage)->load(0, m_loadPool);
|
||||
}
|
||||
}
|
||||
if(newWidth < m_width)
|
||||
@@ -498,8 +546,8 @@ void ImageRingList::setFilesPrivate(const QStringList files, const QString &curr
|
||||
for(const QString &file : files)
|
||||
{
|
||||
ImagePtr ptr = make_shared<Image>(file, i++, this);
|
||||
connect(ptr.get(), SIGNAL(pixmapLoaded(Image*)), this, SLOT(imageLoaded(Image*)));
|
||||
connect(ptr.get(), SIGNAL(thumbnailLoaded(Image*)), this, SIGNAL(thumbnailLoaded(Image*)));
|
||||
connect(ptr.get(), &Image::pixmapLoaded, this, &ImageRingList::imageLoaded);
|
||||
connect(ptr.get(), &Image::thumbnailLoaded, this, &ImageRingList::thumbnailLoaded);
|
||||
m_images.append(ptr);
|
||||
}
|
||||
|
||||
@@ -538,9 +586,8 @@ void ImageRingList::imageLoaded(Image *image)
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::dirChanged(QString dir)
|
||||
void ImageRingList::dirChanged(QString)
|
||||
{
|
||||
m_currentDir = dir;
|
||||
if(m_liveMode)
|
||||
reloadDir();
|
||||
else
|
||||
|
||||
+6
-2
@@ -28,7 +28,7 @@ class Image : public QObject
|
||||
ImageRingList *m_ringList;
|
||||
public:
|
||||
explicit Image(const QString name, int number, ImageRingList *ringList);
|
||||
void load(QThreadPool *pool);
|
||||
void load(int index, QThreadPool *pool);
|
||||
void loadThumbnail(QThreadPool *pool);
|
||||
void release();
|
||||
QString name() const;
|
||||
@@ -79,6 +79,7 @@ public:
|
||||
void setFile(const QString &file);
|
||||
void setFiles(QStringList files);
|
||||
ImagePtr currentImage();
|
||||
QString currentDir() const;
|
||||
void setLiveMode(bool live);
|
||||
void setCalculateStats(bool stats);
|
||||
void setFindPeaks(bool findPeaks);
|
||||
@@ -105,7 +106,10 @@ public slots:
|
||||
void toggleSlideshow(bool start);
|
||||
void increment();
|
||||
void decrement();
|
||||
void prevSubImage();
|
||||
void nextSubImage();
|
||||
void setMarked();
|
||||
void reloadImage();
|
||||
protected:
|
||||
void setFilesPrivate(const QStringList files, const QString ¤tFile = QString());
|
||||
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
|
||||
@@ -117,7 +121,7 @@ signals:
|
||||
void currentImageChanged(int index);
|
||||
protected slots:
|
||||
void imageLoaded(Image *image);
|
||||
void dirChanged(QString dir);
|
||||
void dirChanged(QString);
|
||||
void reloadDir();
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ ImageScrollArea::ImageScrollArea(Database *database, QWidget *parent) : QWidget(
|
||||
layout->addWidget(m_verticalScrollBar, 0, 1);
|
||||
layout->addWidget(m_horizontalScrollBar, 1, 0);
|
||||
|
||||
connect(m_verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
|
||||
connect(m_horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
|
||||
connect(m_verticalScrollBar, &QScrollBar::valueChanged, this, &ImageScrollArea::scrollEvent);
|
||||
connect(m_horizontalScrollBar, &QScrollBar::valueChanged, this, &ImageScrollArea::scrollEvent);
|
||||
|
||||
if(imageWidgetGL)
|
||||
{
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ ImageWidgetGL::ImageWidgetGL(Database *database, QWidget *parent) : QOpenGLWidge
|
||||
m_updateTimer = new QTimer(this);
|
||||
m_updateTimer->setInterval(500);
|
||||
m_updateTimer->setSingleShot(true);
|
||||
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(update()));
|
||||
connect(m_updateTimer, &QTimer::timeout, this, static_cast<void (QOpenGLWidget::*)()>(&ImageWidgetGL::update));
|
||||
setAcceptDrops(true);
|
||||
QTimer::singleShot(1000, [this](){
|
||||
if(!isValid())
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
find_program(XDG-ICON-RESOURCE_EXECUTABLE xdg-icon-resource)
|
||||
find_program(XDG-MIME xdg-mime)
|
||||
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor space.nouspiro.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 resources/space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 resources/space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-MIME} install --novendor space.nouspiro.tenmon.xisf.xml WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
+40
-13
@@ -83,11 +83,10 @@ int loadFITSHeader(fitsfile *file, ImageInfoData &info)
|
||||
return status;
|
||||
}
|
||||
|
||||
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar)
|
||||
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
|
||||
{
|
||||
fitsfile *file;
|
||||
int status = 0;
|
||||
int type = -1;
|
||||
int num = 0;
|
||||
long naxes[3] = {0};
|
||||
|
||||
@@ -105,16 +104,32 @@ bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage>
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
if(status)return checkError();
|
||||
|
||||
int hdutype;
|
||||
int imgtype;
|
||||
int naxis;
|
||||
for(int i=1; i <= num; i++)
|
||||
std::vector<int> imageIdxs;
|
||||
for(int i = 1; i <= num; i++)
|
||||
{
|
||||
fits_movabs_hdu(file, i, IMAGE_HDU, &status);if(status)return checkError();
|
||||
fits_get_hdu_type(file, &type, &status);if(status)return checkError();
|
||||
fits_movabs_hdu(file, i, &hdutype, &status);if(status)return checkError();
|
||||
if(hdutype == IMAGE_HDU)
|
||||
{
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
|
||||
}
|
||||
}
|
||||
info.num = imageIdxs.size();
|
||||
info.index = index;
|
||||
|
||||
if(index >= imageIdxs.size())return false;
|
||||
|
||||
fits_movabs_hdu(file, imageIdxs[index], &hdutype, &status);if(status)return checkError();
|
||||
if(hdutype == IMAGE_HDU)
|
||||
{
|
||||
naxes[0] = naxes[1] = naxes[2] = 0;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();
|
||||
|
||||
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
if(hdutype == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
{
|
||||
RawImage::DataType type;
|
||||
int fitstype;
|
||||
@@ -133,6 +148,10 @@ bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage>
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case LONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TINT;
|
||||
break;
|
||||
case ULONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TUINT;
|
||||
@@ -173,13 +192,18 @@ bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage>
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT16_MIN;
|
||||
}
|
||||
else if(fitstype == TINT)
|
||||
{
|
||||
uint32_t *s = static_cast<uint32_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT32_MIN;
|
||||
}
|
||||
|
||||
if(img.channels() == 1 || planar)
|
||||
image = std::make_shared<RawImage>(std::move(img));
|
||||
else
|
||||
image = RawImage::fromPlanar(img);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
noload:
|
||||
@@ -202,14 +226,15 @@ noload:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar)
|
||||
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image, bool planar, uint32_t index)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path.toLocal8Bit().data());
|
||||
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(0);
|
||||
if(index >= (uint32_t)xisf.imagesCount())return false;
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(index);
|
||||
|
||||
auto fitskeywords = xisfImage.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
@@ -222,6 +247,8 @@ bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
|
||||
info.num = xisf.imagesCount();
|
||||
info.index = index;
|
||||
info.wcs = std::make_shared<WCSDataT>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
|
||||
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
|
||||
@@ -378,7 +405,7 @@ bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage>
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, bool planar)
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar)
|
||||
{
|
||||
bool ret = false;
|
||||
QElapsedTimer timer;
|
||||
@@ -390,12 +417,12 @@ bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImag
|
||||
}
|
||||
else if(path.endsWith(".FIT", Qt::CaseInsensitive) || path.endsWith(".FITS", Qt::CaseInsensitive) || path.endsWith(".FZ", Qt::CaseInsensitive) || path.endsWith(".FTS", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadFITS(path, info, rawImage, planar);
|
||||
ret = loadFITS(path, info, rawImage, planar, index);
|
||||
qDebug() << "LoadFITS" << timer.elapsed();
|
||||
}
|
||||
else if(path.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadXISF(path, info, rawImage, planar);
|
||||
ret = loadXISF(path, info, rawImage, planar, index);
|
||||
qDebug() << "LoadXISF" << timer.elapsed();
|
||||
}
|
||||
else
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ class RawImage;
|
||||
QString makeUNCPath(const QString &path);
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info);
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info);
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, bool planar = false);
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage, int index, bool planar = false);
|
||||
|
||||
#endif // LOADIMAGE_H
|
||||
|
||||
+5
-4
@@ -10,11 +10,12 @@
|
||||
#include "loadimage.h"
|
||||
#include <lcms2.h>
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail) :
|
||||
m_file(makeUNCPath(file)),
|
||||
m_receiver(receiver),
|
||||
m_analyzeLevel(level),
|
||||
m_thumbnail(thumbnail)
|
||||
m_thumbnail(thumbnail),
|
||||
m_index(index)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ void LoadRunable::run()
|
||||
info.info.append({QObject::tr("Filename"), finfo.fileName()});
|
||||
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(!loadImage(m_file, info, rawImage))
|
||||
if(!loadImage(m_file, info, rawImage, m_index))
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")});
|
||||
|
||||
|
||||
@@ -187,7 +188,7 @@ void ConvertRunable::run()
|
||||
|
||||
ImageInfoData imageinfo;
|
||||
std::shared_ptr<RawImage> rawimage;
|
||||
loadImage(m_infile, imageinfo, rawimage);
|
||||
loadImage(m_infile, imageinfo, rawimage, 0);
|
||||
QFileInfo info(m_outfile);
|
||||
info.dir().mkpath(".");
|
||||
|
||||
|
||||
+2
-1
@@ -15,8 +15,9 @@ class LoadRunable : public QRunnable
|
||||
Image *m_receiver;
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
bool m_thumbnail;
|
||||
int m_index = 0;
|
||||
public:
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail = false);
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, int index, bool thumbnail = false);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
|
||||
+46
-45
@@ -95,7 +95,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
|
||||
m_ringList = new ImageRingList(m_database, nameFilter, this);
|
||||
m_filesystem = new FilesystemWidget(m_ringList, this);
|
||||
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
|
||||
connect(m_filesystem, &FilesystemWidget::fileSelected, this, static_cast<void (MainWindow::*)(int)>(&MainWindow::loadFile));
|
||||
connect(m_filesystem, &FilesystemWidget::sortChanged, m_ringList, &ImageRingList::setSort);
|
||||
connect(m_filesystem, &FilesystemWidget::reverseSort, m_ringList, &ImageRingList::reverseSort);
|
||||
|
||||
@@ -106,7 +106,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
connect(m_filetree, &Filetree::indexDirectory, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::indexDir));
|
||||
|
||||
m_databaseView = new DataBaseView(m_database, this);
|
||||
connect(m_databaseView, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
|
||||
connect(m_databaseView, &DataBaseView::loadFile, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
|
||||
#ifdef PLATESOLVER
|
||||
_plateSolving = new PlateSolving(this);
|
||||
@@ -114,6 +114,21 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
_plateSolving->hide();
|
||||
#endif
|
||||
|
||||
QToolBar *navigationToolbar = new QToolBar(tr("Navigation toolbar"), this);
|
||||
navigationToolbar->setObjectName("navigationtoolbar");
|
||||
navigationToolbar->hide();
|
||||
QAction *prevAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowLeft), tr("Previous image"));
|
||||
prevAction->setShortcuts({Qt::Key_Left, Qt::Key_Up});
|
||||
QAction *nextAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("Next image"));
|
||||
nextAction->setShortcuts({Qt::Key_Right, Qt::Key_Down});
|
||||
QAction *prevSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowUp), tr("Prev sub image"), Qt::Key_PageUp);
|
||||
QAction *nextSubAction = navigationToolbar->addAction(style()->standardIcon(QStyle::SP_ArrowDown), tr("Next sub image"), Qt::Key_PageDown);
|
||||
connect(prevAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::decrement));
|
||||
connect(nextAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
|
||||
connect(prevSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::prevSubImage));
|
||||
connect(nextSubAction, &QAction::triggered, m_ringList, static_cast<void (ImageRingList::*)()>(&ImageRingList::nextSubImage));
|
||||
|
||||
addToolBar(Qt::TopToolBarArea, navigationToolbar);
|
||||
addToolBar(Qt::TopToolBarArea, m_stretchPanel);
|
||||
|
||||
QDockWidget *filesystemDock = new QDockWidget(tr("Filesystem"), this);
|
||||
@@ -151,20 +166,21 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, histogram, &Histogram::imageLoaded);
|
||||
#ifdef PLATESOLVER
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, _plateSolving, &PlateSolving::imageLoaded);
|
||||
connect(_plateSolving, &PlateSolving::headerUpdated, m_ringList, &ImageRingList::reloadImage);
|
||||
#endif
|
||||
connect(m_image, &ImageScrollArea::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
|
||||
QMenu *fileMenu = new QMenu(tr("File"), this);
|
||||
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, SLOT(loadFile()));
|
||||
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, static_cast<void (MainWindow::*)()>(&MainWindow::loadFile));
|
||||
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
|
||||
QAction *saveAs = fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, SLOT(saveAs()));
|
||||
QAction *saveAs = fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, &MainWindow::saveAs);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, SLOT(copyMarked()));
|
||||
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, SLOT(moveMarked()));
|
||||
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, &MainWindow::copyMarked);
|
||||
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, &MainWindow::moveMarked);
|
||||
fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
||||
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
||||
fileMenu->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir));
|
||||
fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex);
|
||||
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
||||
fileMenu->addAction(tr("Batch processing"), Qt::Key_B | Qt::CTRL, [this](){
|
||||
BatchProcessing *batchProcessing = new BatchProcessing(m_database, this);
|
||||
@@ -172,9 +188,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
delete batchProcessing;
|
||||
});
|
||||
fileMenu->addSeparator();
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, &MainWindow::liveMode);
|
||||
liveModeAction->setCheckable(true);
|
||||
QAction *exitAction = fileMenu->addAction(tr("Exit"), this, SLOT(close()));
|
||||
QAction *exitAction = fileMenu->addAction(tr("Exit"), this, &MainWindow::close);
|
||||
exitAction->setShortcut(QKeySequence::Quit);
|
||||
menuBar()->addMenu(fileMenu);
|
||||
|
||||
@@ -182,6 +198,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
editMenu->addAction(tr("Settings"), this, &MainWindow::showSettingsDialog);
|
||||
menuBar()->addMenu(editMenu);
|
||||
|
||||
QMenu *navigationMenu = new QMenu(tr("Navigation"), this);
|
||||
navigationMenu->addAction(prevAction);
|
||||
navigationMenu->addAction(nextAction);
|
||||
navigationMenu->addAction(prevSubAction);
|
||||
navigationMenu->addAction(nextSubAction);
|
||||
menuBar()->addMenu(navigationMenu);
|
||||
|
||||
QMenu *viewMenu = new QMenu(tr("View"), this);
|
||||
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn, m_image, &ImageScrollArea::zoomIn);
|
||||
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut, m_image, &ImageScrollArea::zoomOut);
|
||||
@@ -229,7 +252,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
settings.setValue("mainwindow/colormap", data);
|
||||
});
|
||||
|
||||
|
||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), Qt::Key_F2, [this](bool checked){
|
||||
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
||||
m_image->allocateThumbnails(m_ringList->imageNames());
|
||||
@@ -240,14 +262,16 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
thumbnailsAction->setCheckable(true);
|
||||
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), Qt::Key_F3, m_ringList, &ImageRingList::toggleSlideshow);
|
||||
slideshowAction->setCheckable(true);
|
||||
viewMenu->addSeparator();
|
||||
viewMenu->addActions(m_stretchPanel->actions());
|
||||
menuBar()->addMenu(viewMenu);
|
||||
|
||||
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
||||
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, SLOT(markImage()));
|
||||
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, SLOT(unmarkImage()));
|
||||
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, &MainWindow::markImage);
|
||||
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, &MainWindow::unmarkImage);
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, SLOT(markAndNext()));
|
||||
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, SLOT(unmarkAndNext()));
|
||||
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, &MainWindow::markAndNext);
|
||||
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, &MainWindow::unmarkAndNext);
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Show marked list"), this, &MainWindow::showMarkFilesDialog);
|
||||
QAction *openMarked = selectMenu->addAction(tr("Open marked"), m_ringList, &ImageRingList::setMarked);
|
||||
@@ -282,6 +306,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
QMenu *dockMenu = new QMenu(tr("Docks"), this);
|
||||
dockMenu->addAction(infoDock->toggleViewAction());
|
||||
dockMenu->addAction(m_stretchPanel->toggleViewAction());
|
||||
dockMenu->addAction(navigationToolbar->toggleViewAction());
|
||||
dockMenu->addAction(filesystemDock->toggleViewAction());
|
||||
dockMenu->addAction(databaseViewDock->toggleViewAction());
|
||||
dockMenu->addAction(filetreeDock->toggleViewAction());
|
||||
@@ -292,7 +317,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
menuBar()->addMenu(dockMenu);
|
||||
|
||||
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
|
||||
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog help(this); help.exec(); });
|
||||
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog *help = new HelpDialog(this); help->show(); });
|
||||
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
|
||||
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
|
||||
helpMenu->addAction(tr("Check for update"), this, &MainWindow::checkNewVersion);
|
||||
@@ -345,32 +370,6 @@ MainWindow::~MainWindow()
|
||||
delete m_database;
|
||||
}
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_Left:
|
||||
case Qt::Key_Up:
|
||||
m_ringList->decrement();
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
case Qt::Key_Down:
|
||||
m_ringList->increment();
|
||||
break;
|
||||
default:
|
||||
event->ignore();
|
||||
break;
|
||||
}
|
||||
|
||||
if(event->isAccepted())
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
void MainWindow::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void MainWindow::setupSigterm()
|
||||
{
|
||||
#ifdef __linux__
|
||||
@@ -387,7 +386,7 @@ void MainWindow::setupSigterm()
|
||||
|
||||
::socketpair(AF_UNIX, SOCK_STREAM, 0, socketPair);
|
||||
socketNotifier = new QSocketNotifier(socketPair[1], QSocketNotifier::Read, this);
|
||||
connect(socketNotifier, SIGNAL(activated(int)), this, SLOT(socketNotify()));
|
||||
connect(socketNotifier, &QSocketNotifier::activated, this, &MainWindow::socketNotify);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -811,8 +810,10 @@ void MainWindow::updateWindowTitle()
|
||||
ImagePtr ptr = m_ringList->currentImage();
|
||||
if(ptr)
|
||||
{
|
||||
QFileInfo info(ptr->name());
|
||||
QString title = info.fileName();
|
||||
QDir dir(m_ringList->currentDir());
|
||||
QString title = dir.relativeFilePath(ptr->name());
|
||||
if(ptr->info().num > 1)
|
||||
title += QString(" [%1/%2]").arg(ptr->info().index + 1).arg(ptr->info().num);
|
||||
if(m_database->isMarked(ptr->name()))
|
||||
title += " *";
|
||||
setWindowTitle(title);
|
||||
|
||||
@@ -34,8 +34,6 @@ public:
|
||||
MainWindow(QWidget *parent = 0);
|
||||
~MainWindow() override;
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void keyReleaseEvent(QKeyEvent *event) override;
|
||||
void setupSigterm();
|
||||
static void signalHandler(int);
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
@@ -44,6 +44,7 @@ PlateSolving::PlateSolving(QWidget *parent)
|
||||
connect(_solver, &Solver::solvingDone, this, &PlateSolving::solvingDone);
|
||||
connect(_solver, &Solver::extractionDone, this, &PlateSolving::extractionDone);
|
||||
connect(_solver, &Solver::logOutput, [this](const QString &log){ _ui->log->appendPlainText(log); });
|
||||
connect(_solver, &Solver::headerUpdated, this, &PlateSolving::headerUpdated);
|
||||
}
|
||||
|
||||
PlateSolving::~PlateSolving()
|
||||
|
||||
@@ -32,6 +32,8 @@ public slots:
|
||||
void updateHeader();
|
||||
void imageLoaded(Image *image);
|
||||
void settings();
|
||||
signals:
|
||||
void headerUpdated(const QString &path);
|
||||
private:
|
||||
Ui::PlateSolving *_ui;
|
||||
};
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
#include "rawimage.h"
|
||||
#include <cstring>
|
||||
#include <lcms2.h>
|
||||
#include <algorithm>
|
||||
#ifndef NO_QT
|
||||
#include <lcms2.h>
|
||||
#include <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFloat16>
|
||||
@@ -55,7 +55,7 @@ void RawImage::allocate(uint32_t w, uint32_t h, uint32_t ch, DataType type)
|
||||
m_width = w;
|
||||
m_height = h;
|
||||
m_channels = ch;
|
||||
m_ch = ch == 3 ? 4 : ch;
|
||||
m_ch = ch > 1 ? 4 : ch;
|
||||
m_origType = m_type = type;
|
||||
m_pixels = std::make_unique<PixelType[]>((size_t)m_width * m_height * m_ch * typeSize(type));
|
||||
}
|
||||
@@ -1071,7 +1071,6 @@ void RawImage::setICCProfile(const QByteArray &icc)
|
||||
if(icc.size())
|
||||
m_iccProfile = std::vector<uint8_t>(icc.begin(), icc.end());
|
||||
}
|
||||
#endif
|
||||
|
||||
void RawImage::setICCProfile(const LibXISF::ByteArray &icc)
|
||||
{
|
||||
@@ -1180,6 +1179,7 @@ void RawImage::generateLUT()
|
||||
cmsCloseProfile(inProfile);
|
||||
cmsCloseProfile(outProfile);
|
||||
}
|
||||
#endif
|
||||
|
||||
void RawImage::applySTF(const MTFParam &mtfParams)
|
||||
{
|
||||
|
||||
+1
-1
@@ -125,10 +125,10 @@ public:
|
||||
bool valid() const;
|
||||
#ifndef NO_QT
|
||||
void setICCProfile(const QByteArray &icc);
|
||||
#endif
|
||||
void setICCProfile(const LibXISF::ByteArray &icc);
|
||||
void convertTosRGB();
|
||||
void generateLUT();
|
||||
#endif
|
||||
void applySTF(const MTFParam &mtfParams);
|
||||
MTFParam calcMTFParams(bool linked = false, bool debayer = false) const;
|
||||
const std::vector<uint16_t>& getLUT() const;
|
||||
|
||||
+95
-2
@@ -26,7 +26,9 @@ ScriptEngine::ScriptEngine(Database *database, BatchProcessing *parent)
|
||||
QJSValue core = _jsEngine->newQObject(this);
|
||||
_jsEngine->globalObject().setProperty("core", core);
|
||||
QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject);
|
||||
QJSValue textFile = _jsEngine->newQMetaObject(&TextFile::staticMetaObject);
|
||||
_jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject);
|
||||
_jsEngine->globalObject().setProperty("TextFile", textFile);
|
||||
_semaphore.release(_pool->maxThreadCount());
|
||||
_pool->setThreadPriority(QThread::LowPriority);
|
||||
|
||||
@@ -134,6 +136,41 @@ QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, i
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::question(const QString &question, const QStringList &buttons, const QString &title) const
|
||||
{
|
||||
QJSValue ret;
|
||||
QMetaObject::invokeMethod(_parent, "question", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, question), Q_ARG(QStringList, buttons), Q_ARG(QString, title));
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ScriptEngine::plot(const QJSValue &graph)
|
||||
{
|
||||
QVariant graphV = graph.toVariant(QJSValue::ConvertJSObjects);
|
||||
if(graphV.isValid())
|
||||
QMetaObject::invokeMethod(_parent, "plot", Qt::QueuedConnection, Q_ARG(QVariant, graphV));
|
||||
else
|
||||
logError("Invalid value to be plotted");
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::openFile(const QString &fileName, const QString &mode)
|
||||
{
|
||||
QFileInfo info(fileName);
|
||||
if(!info.isAbsolute())
|
||||
info = QFileInfo(outputDir() + fileName);
|
||||
|
||||
TextFile *textFile = new TextFile;
|
||||
if(textFile->open(info.absoluteFilePath(), mode))
|
||||
{
|
||||
return _jsEngine->newQObject(textFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
logError("Failed to open file " + fileName);
|
||||
delete textFile;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async)
|
||||
{
|
||||
QString path;
|
||||
@@ -226,7 +263,7 @@ 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());
|
||||
_solver->setSearchPosition(solution.property("ra").toNumber() / 15.0, solution.property("dec").toNumber());
|
||||
|
||||
if(solution.hasProperty("pixscale") && solution.property("pixscale").isNumber())
|
||||
{
|
||||
@@ -737,7 +774,7 @@ QJSValue File::stats()
|
||||
{
|
||||
ImageInfoData info;
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
loadImage(_path, info, rawImage);
|
||||
loadImage(_path, info, rawImage, 0);
|
||||
rawImage->calcStats();
|
||||
RawImage::Stats stats = rawImage->imageStats();
|
||||
_stats = _engine->newObject();
|
||||
@@ -820,4 +857,60 @@ void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, con
|
||||
_update.append({key.toLatin1(), value, comment.toLatin1()});
|
||||
}
|
||||
|
||||
bool TextFile::open(const QString &path, const QString &mode)
|
||||
{
|
||||
_fr.setFileName(path);
|
||||
QIODevice::OpenMode openMode;
|
||||
if(mode == "r")
|
||||
openMode = QIODevice::ReadOnly;
|
||||
else if(mode == "w")
|
||||
openMode = QIODevice::WriteOnly;
|
||||
else if(mode == "a")
|
||||
openMode = QIODevice::WriteOnly | QIODevice::Append;
|
||||
else if(mode == "r+")
|
||||
openMode = QIODevice::ReadWrite | QIODevice::ExistingOnly;
|
||||
else if(mode == "w+")
|
||||
openMode = QIODevice::ReadWrite;
|
||||
else if(mode == "a+")
|
||||
openMode = QIODevice::ReadWrite | QIODevice::Append;
|
||||
else
|
||||
return false;
|
||||
|
||||
openMode |= QIODevice::Text;//always open as text
|
||||
return _fr.open(openMode);
|
||||
}
|
||||
|
||||
void TextFile::write(const QString &data)
|
||||
{
|
||||
_fr.write(data.toUtf8());
|
||||
}
|
||||
|
||||
QString TextFile::read(int maxlen)
|
||||
{
|
||||
QByteArray data = _fr.read(maxlen);
|
||||
return data;
|
||||
}
|
||||
|
||||
QString TextFile::readLine()
|
||||
{
|
||||
QByteArray data = _fr.readLine();
|
||||
return QString::fromUtf8(data);
|
||||
}
|
||||
|
||||
QString TextFile::readAll()
|
||||
{
|
||||
QByteArray data = _fr.readAll();
|
||||
return QString::fromUtf8(data);
|
||||
}
|
||||
|
||||
bool TextFile::seek(qint64 offset)
|
||||
{
|
||||
return _fr.seek(offset);
|
||||
}
|
||||
|
||||
qint64 TextFile::pos()
|
||||
{
|
||||
return _fr.pos();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-1
@@ -8,7 +8,7 @@
|
||||
#include <QThreadPool>
|
||||
#include <QSemaphore>
|
||||
#include "database.h"
|
||||
#include "imageinfo.h"
|
||||
#include "imageinfodata.h"
|
||||
|
||||
class BatchProcessing;
|
||||
class Solver;
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
Q_INVOKABLE QJSValue getInt(const QString &label = QString(), int value = 0);
|
||||
Q_INVOKABLE QJSValue getFloat(const QString &label = QString(), double value = 0, int decimals = 3) const;
|
||||
Q_INVOKABLE QJSValue getItem(const QStringList &items, const QString &label = "", int current = 0) const;
|
||||
Q_INVOKABLE QJSValue question(const QString &question, const QStringList &buttons = {"ok"}, const QString &title = "") const;
|
||||
Q_INVOKABLE void plot(const QJSValue &pointsArray);
|
||||
Q_INVOKABLE QJSValue openFile(const QString &fileName, const QString &mode = "r");
|
||||
bool convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async);
|
||||
#ifdef PLATESOLVER
|
||||
Q_INVOKABLE void setSolverProfile(int index);
|
||||
@@ -142,6 +145,20 @@ public:
|
||||
Q_INVOKABLE void addKeyword(const QString &key, const QVariant &value, const QString &comment = QString());
|
||||
};
|
||||
|
||||
class TextFile : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QFile _fr;
|
||||
public:
|
||||
bool open(const QString &path, const QString &mode);
|
||||
Q_INVOKABLE void write(const QString &data);
|
||||
Q_INVOKABLE QString read(int maxlen);
|
||||
Q_INVOKABLE QString readLine();
|
||||
Q_INVOKABLE QString readAll();
|
||||
Q_INVOKABLE bool seek(qint64 offset);
|
||||
Q_INVOKABLE qint64 pos();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SCRIPTENGINE_H
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
core.log("This script convert any FITS file into XISF with ZSTD compression");
|
||||
|
||||
if(files.length == 0)
|
||||
{
|
||||
core.log("No input files");
|
||||
throw "";
|
||||
}
|
||||
|
||||
let compression = {"compressionType": "zstd+sh"};
|
||||
|
||||
for(file of files)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
core.log("Measure HFR and eccentricity of stars");
|
||||
|
||||
var chart = {
|
||||
"title": "Measure stars",
|
||||
"legend": {"visible": true, "align": "left"},
|
||||
"series": [
|
||||
{
|
||||
"title": "HFR",
|
||||
"type": "bar",
|
||||
"y":[]
|
||||
},
|
||||
{
|
||||
"title": "Ecc",
|
||||
"type": "bar",
|
||||
"y":[]
|
||||
},
|
||||
{
|
||||
"title": "Star count",
|
||||
"type": "linePoints",
|
||||
"y":[],
|
||||
"y2": true,
|
||||
"bestFit": true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
core.setSolverProfile(5);
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||
{
|
||||
var stars = file.extractStars(true);
|
||||
var sumHFR = 0;
|
||||
var ecc = 0;
|
||||
for(star of stars)
|
||||
{
|
||||
sumHFR += star.HFR;
|
||||
ecc += Math.sqrt(1 - (star.b * star.b) / (star.a * star.a));
|
||||
}
|
||||
chart.series[0].y.push(sumHFR / stars.length);
|
||||
chart.series[1].y.push(ecc / stars.length);
|
||||
chart.series[2].y.push(stars.length);
|
||||
|
||||
core.log(file.fileName() + " Stars:" + stars.length + " HFR: " + sumHFR / stars.length + " Ecc: " + ecc / stars.length);
|
||||
}
|
||||
}
|
||||
|
||||
core.plot(chart);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
core.log("Script to modify FITS header in FITS and XISF files");
|
||||
|
||||
function checkFITS(key)
|
||||
{
|
||||
const noEditableKey = ["SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"];
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
core.log("Plate solve and update solution");
|
||||
|
||||
var first = true;
|
||||
|
||||
var update = core.question("Update FITS header with solution?", ["yes", "no"], "Update FITS header") == "yes";
|
||||
var blind = core.question("Do blind solve every image?", ["yes", "no"], "Blind solve?") == "yes";
|
||||
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||
{
|
||||
var solution = file.solve(update);
|
||||
if(first && !blind)
|
||||
{
|
||||
core.setStartingSolution(solution);
|
||||
first = false;
|
||||
}
|
||||
core.log(file.fileName() + " " + "RA: " + (solution.ra / 15) + "h DEC: " + solution.dec + "deg");
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,7 @@
|
||||
<file>convert to XISF</file>
|
||||
<file>median</file>
|
||||
<file>modify FITS header</file>
|
||||
<file>measure HFR</file>
|
||||
<file>plate solve</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
+65
-3
@@ -10,12 +10,15 @@
|
||||
#include <QMessageBox>
|
||||
#include <QDir>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
#include <QColorDialog>
|
||||
#include "rawimage.h"
|
||||
|
||||
extern int DEFAULT_WIDTH;
|
||||
extern double SATURATION;
|
||||
extern int FILTERING;
|
||||
extern bool BESTFIT;
|
||||
extern QMap<QString, QColor> headerHighlight;
|
||||
|
||||
class EvenNumber : public QSpinBox
|
||||
{
|
||||
@@ -86,6 +89,45 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
m_bestFit->setToolTip(tr("Set Best Fit zoom level when opening new image."));
|
||||
m_bestFit->setChecked(BESTFIT);
|
||||
|
||||
m_headerHighlight = new QListWidget(this);
|
||||
m_headerHighlight->setToolTip(tr("List of FITS keywords that will be highlighted in Image info"));
|
||||
for(auto i = headerHighlight.begin(); i != headerHighlight.end(); i++)
|
||||
{
|
||||
QListWidgetItem *item = new QListWidgetItem(m_headerHighlight);
|
||||
item->setText(i.key());
|
||||
item->setBackground(i.value());
|
||||
}
|
||||
m_keyword = new QLineEdit(this);
|
||||
m_keyword->setPlaceholderText(tr("FITS keyword"));
|
||||
QPushButton *color = new QPushButton(this);
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(m_color);
|
||||
color->setIcon(pix);
|
||||
connect(color, &QPushButton::clicked, [this, color](){
|
||||
QColor rgb = QColorDialog::getColor(m_color, this);
|
||||
if(rgb.isValid())
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(rgb);
|
||||
color->setIcon(pix);
|
||||
m_color = rgb;
|
||||
}
|
||||
});
|
||||
|
||||
QPushButton *add = new QPushButton(tr("Add keyword highlight"), this);
|
||||
connect(add, &QPushButton::clicked, [this](){
|
||||
auto list = m_headerHighlight->findItems(m_keyword->text(), Qt::MatchFixedString | Qt::MatchCaseSensitive);
|
||||
if(list.size())return;
|
||||
QListWidgetItem *item = new QListWidgetItem(m_headerHighlight);
|
||||
item->setText(m_keyword->text());
|
||||
item->setBackground(m_color);
|
||||
});
|
||||
QPushButton *remove = new QPushButton(tr("Remove keyword highlight"), this);
|
||||
connect(remove, &QPushButton::clicked, [this](){
|
||||
auto list = m_headerHighlight->selectedItems();
|
||||
for(auto item : list)
|
||||
delete item;
|
||||
});
|
||||
|
||||
layout->addRow(tr("Image preload count"), m_preloadImages);
|
||||
layout->addRow(tr("Thumbnails size"), m_thumSize);
|
||||
@@ -95,6 +137,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
layout->addRow(m_qualityThumbnail);
|
||||
layout->addRow(m_useNativeDialog);
|
||||
layout->addRow(m_bestFit);
|
||||
layout->addRow(new QLabel(tr("FITS header highlight"), this));
|
||||
layout->addRow(m_headerHighlight);
|
||||
layout->addRow(m_keyword, color);
|
||||
layout->addRow(add, remove);
|
||||
|
||||
#ifdef Q_OS_WIN64
|
||||
QPushButton *installThumbnailer = new QPushButton(tr("Install"), this);
|
||||
@@ -125,6 +171,11 @@ void SettingsDialog::loadSettings()
|
||||
FILTERING = settings.value("settings/filtering", FILTERING).toInt();
|
||||
QUALITY_RESIZE = settings.value("settings/qualitythumbnail", QUALITY_RESIZE).toBool();
|
||||
BESTFIT = settings.value("settings/bestfit", BESTFIT).toBool();
|
||||
QStringList keywords = settings.value("settings/headerhighlightkeywords").toStringList();
|
||||
QStringList colors = settings.value("settings/headerhighlightcolors").toStringList();
|
||||
for(int i = 0; i < std::min(keywords.size(), colors.size()); i++)
|
||||
headerHighlight.insert(keywords[i], QColor::fromString(colors[i]));
|
||||
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, settings.value("settings/dontusenativedialogs", false).toBool());
|
||||
}
|
||||
|
||||
@@ -150,10 +201,10 @@ void SettingsDialog::installThumbnailer()
|
||||
|
||||
QProcess regsvr;
|
||||
int ret = regsvr.execute("regsvr32.exe", {"/s", path});
|
||||
if(ret)
|
||||
{
|
||||
if(ret == 0)
|
||||
QMessageBox::information(this, tr("Thumbnail support"), tr("Thumbnail generation support sucessufully installed."));
|
||||
else
|
||||
QMessageBox::critical(this, tr("Error"), tr("Failed to register thumbnailer. %1").arg(ret));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -175,4 +226,15 @@ void SettingsDialog::saveSettings()
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, m_useNativeDialog->isChecked());
|
||||
if(DEFAULT_WIDTH != m_preloadImages->value())
|
||||
emit preloadChanged(m_preloadImages->value());
|
||||
|
||||
headerHighlight.clear();
|
||||
QStringList colors;
|
||||
for(int i = 0; i < m_headerHighlight->count(); i++)
|
||||
{
|
||||
auto item = m_headerHighlight->item(i);
|
||||
colors.push_back(item->background().color().name());
|
||||
headerHighlight[item->text()] = item->background().color();
|
||||
}
|
||||
settings.setValue("settings/headerhighlightkeywords", headerHighlight.keys());
|
||||
settings.setValue("settings/headerhighlightcolors", colors);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QListWidget>
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
@@ -28,6 +29,9 @@ private:
|
||||
QCheckBox *m_qualityThumbnail;
|
||||
QComboBox *m_filtering;
|
||||
QCheckBox *m_bestFit;
|
||||
QListWidget *m_headerHighlight;
|
||||
QColor m_color = Qt::yellow;
|
||||
QLineEdit *m_keyword;
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
|
||||
+2
-1
@@ -41,7 +41,7 @@ bool Solver::loadImage(const QString &path)
|
||||
_loaded = false;
|
||||
std::shared_ptr<RawImage> image;
|
||||
ImageInfoData info;
|
||||
if(::loadImage(path, info, image, true))
|
||||
if(::loadImage(path, info, image, 0, true))
|
||||
{
|
||||
return loadImage(image, path);
|
||||
}
|
||||
@@ -188,6 +188,7 @@ bool Solver::updateHeader(QString &error)
|
||||
modify.updateKeyword("EQUINOX", 2000, QByteArray("Equinox of coordinates"));
|
||||
bool ret = file.modifyFITSRecords(&modify);
|
||||
if(!ret)error = tr("Failed to update file header");
|
||||
else emit headerUpdated(_path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ public slots:
|
||||
signals:
|
||||
void solvingDone();
|
||||
void extractionDone();
|
||||
void headerUpdated(const QString &path);
|
||||
void logOutput(const QString &log);
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,17 @@
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="20250429" date="2025-04-29">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add ability to load multiple images in single file</li>
|
||||
<li>New plot() and question() script methods</li>
|
||||
<li>Color highlight of FITS keywords</li>
|
||||
<li>New scripts to batch platesolve and measure stars</li>
|
||||
<li>Stretch toolbar can now be vertical</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20250318" date="2025-03-18">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="image/x-xisf">
|
||||
<comment>Extensible Image Serialization Format</comment>
|
||||
<glob pattern="*.xisf"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
+78
-12
@@ -12,7 +12,7 @@ static float clamp(float x)
|
||||
|
||||
STFSlider::STFSlider(const QColor &color, QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
setMinimumWidth(100);
|
||||
setMouseTracking(true);
|
||||
|
||||
@@ -64,12 +64,51 @@ void STFSlider::setMTFParams(float low, float mid, float high)
|
||||
update();
|
||||
}
|
||||
|
||||
void STFSlider::orientationChanged(Qt::Orientations orientation)
|
||||
{
|
||||
m_orientation = orientation;
|
||||
if(m_orientation == Qt::Horizontal)
|
||||
{
|
||||
if(m_color == Qt::white)
|
||||
{
|
||||
setMaximumSize(QWIDGETSIZE_MAX, 16);
|
||||
setMinimumSize(16, 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
setMaximumSize(QWIDGETSIZE_MAX, 10);
|
||||
setMinimumSize(10, 10);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(m_color == Qt::white)
|
||||
{
|
||||
setMaximumSize(16, QWIDGETSIZE_MAX);
|
||||
setMinimumSize(16, 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
setMaximumSize(10, QWIDGETSIZE_MAX);
|
||||
setMinimumSize(10, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void STFSlider::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
QRect rect = event->rect();
|
||||
qreal w = rect.width() - 1;
|
||||
qreal h = rect.height();
|
||||
if(m_orientation == Qt::Vertical)
|
||||
{
|
||||
rect = rect.transposed();
|
||||
painter.rotate(90);
|
||||
w = rect.width() - 1;
|
||||
h = rect.height();
|
||||
painter.translate(0, -h);
|
||||
}
|
||||
QLinearGradient gradient(rect.topLeft(), rect.topRight());
|
||||
gradient.setColorAt(0, Qt::black);
|
||||
for(int i=1; i<=32; i++)
|
||||
@@ -93,6 +132,11 @@ void STFSlider::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
painter.setPen(p < m_threshold ? Qt::white : Qt::black);
|
||||
painter.resetTransform();
|
||||
if(m_orientation == Qt::Vertical)
|
||||
{
|
||||
painter.rotate(90);
|
||||
painter.translate(0, -h);
|
||||
}
|
||||
painter.translate(w*p, 0);
|
||||
painter.drawPath(tick);
|
||||
};
|
||||
@@ -105,15 +149,26 @@ void STFSlider::paintEvent(QPaintEvent *event)
|
||||
|
||||
void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
const qreal x = event->position().x();
|
||||
if(std::abs(m_blackPoint*width() - x) < 5 ||
|
||||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5 ||
|
||||
std::abs(m_whitePoint*width() - x) < 5)
|
||||
setCursor(Qt::SplitHCursor);
|
||||
qreal x,w;
|
||||
if(m_orientation == Qt::Horizontal)
|
||||
{
|
||||
x = event->position().x();
|
||||
w = width();
|
||||
}
|
||||
else
|
||||
{
|
||||
x = event->position().y();
|
||||
w = height();
|
||||
}
|
||||
|
||||
if(std::abs(m_blackPoint*w - x) < 5 ||
|
||||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*w - x) < 5 ||
|
||||
std::abs(m_whitePoint*w - x) < 5)
|
||||
setCursor(m_orientation == Qt::Horizontal ? Qt::SplitHCursor : Qt::SplitVCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
qreal xw = x/width();
|
||||
qreal xw = x/w;
|
||||
if(event->modifiers() & Qt::ShiftModifier && !m_fineTune)
|
||||
{
|
||||
m_fineTune = true;
|
||||
@@ -154,18 +209,29 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void STFSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
const qreal x = event->position().x();
|
||||
qreal x,w;
|
||||
if(m_orientation == Qt::Horizontal)
|
||||
{
|
||||
x = event->position().x();
|
||||
w = width();
|
||||
}
|
||||
else
|
||||
{
|
||||
x = event->position().y();
|
||||
w = height();
|
||||
}
|
||||
|
||||
if(event->modifiers() & Qt::ShiftModifier)
|
||||
{
|
||||
m_fineTune = true;
|
||||
m_fineTuneX = x/width();
|
||||
m_fineTuneX = x/w;
|
||||
}
|
||||
|
||||
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5)
|
||||
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*w - x) < 5)
|
||||
m_grabbed = 1;
|
||||
else if(std::abs(m_blackPoint*width() - x) < 5)
|
||||
else if(std::abs(m_blackPoint*w - x) < 5)
|
||||
m_grabbed = 0;
|
||||
else if(std::abs(m_whitePoint*width() - x) < 5)
|
||||
else if(std::abs(m_whitePoint*w - x) < 5)
|
||||
m_grabbed = 2;
|
||||
else
|
||||
m_grabbed = -1;
|
||||
|
||||
@@ -15,12 +15,15 @@ class STFSlider : public QWidget
|
||||
float m_fineTuneX;
|
||||
QColor m_color;
|
||||
float m_threshold;
|
||||
Qt::Orientations m_orientation = Qt::Horizontal;
|
||||
public:
|
||||
explicit STFSlider(const QColor &color = Qt::white, QWidget *parent = nullptr);
|
||||
float blackPoint() const;
|
||||
float midPoint() const;
|
||||
float whitePoint() const;
|
||||
void setMTFParams(float low, float mid, float high);
|
||||
public slots:
|
||||
void orientationChanged(Qt::Orientations orientation);
|
||||
signals:
|
||||
void paramChanged(float blackPoint, float midPoint, float whitePoint);
|
||||
protected:
|
||||
|
||||
+12
-16
@@ -6,17 +6,6 @@
|
||||
#include <QStyle>
|
||||
#include "imageringlist.h"
|
||||
|
||||
const float BLACK_POINT_SIGMA = -2.8f;
|
||||
const float MAD_TO_SIGMA = 1.4826f;
|
||||
const float TARGET_BACKGROUND = 0.25f;
|
||||
|
||||
float MTF(float x, float m)
|
||||
{
|
||||
if(x < 0)return 0;
|
||||
if(x > 1)return 1;
|
||||
return ((m - 1) * x) / ((2 * m - 1) * x - m);
|
||||
}
|
||||
|
||||
StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar"), parent)
|
||||
{
|
||||
setObjectName("stretchtoolbar");
|
||||
@@ -24,16 +13,23 @@ StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar")
|
||||
QVBoxLayout *vbox1 = new QVBoxLayout(lum);
|
||||
m_stfSlider = new STFSlider(Qt::white, this);
|
||||
vbox1->addWidget(m_stfSlider);
|
||||
connect(this, &StretchToolbar::orientationChanged, m_stfSlider, &STFSlider::orientationChanged);
|
||||
|
||||
m_stfSliderR = new STFSlider(Qt::red, this);
|
||||
m_stfSliderG = new STFSlider(Qt::green, this);
|
||||
m_stfSliderB = new STFSlider(Qt::blue, this);
|
||||
QWidget *rgb = new QWidget(this);
|
||||
QVBoxLayout *vbox2 = new QVBoxLayout(rgb);
|
||||
vbox2->setSpacing(0);
|
||||
vbox2->addWidget(m_stfSliderR);
|
||||
vbox2->addWidget(m_stfSliderG);
|
||||
vbox2->addWidget(m_stfSliderB);
|
||||
QBoxLayout *box2 = new QBoxLayout(orientation() == Qt::Horizontal ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight, rgb);
|
||||
box2->setSpacing(0);
|
||||
box2->addWidget(m_stfSliderR);
|
||||
box2->addWidget(m_stfSliderG);
|
||||
box2->addWidget(m_stfSliderB);
|
||||
connect(this, &StretchToolbar::orientationChanged, m_stfSliderR, &STFSlider::orientationChanged);
|
||||
connect(this, &StretchToolbar::orientationChanged, m_stfSliderG, &STFSlider::orientationChanged);
|
||||
connect(this, &StretchToolbar::orientationChanged, m_stfSliderB, &STFSlider::orientationChanged);
|
||||
connect(this, &StretchToolbar::orientationChanged, [box2](Qt::Orientations orientation){
|
||||
box2->setDirection(orientation == Qt::Horizontal ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
|
||||
});
|
||||
|
||||
m_stack = new QStackedWidget(this);
|
||||
m_stack->addWidget(lum);
|
||||
|
||||
@@ -4,7 +4,7 @@ if(BUILD_THUMBNAILER)
|
||||
if(WIN32)
|
||||
add_library(tenmonthumbnailer SHARED
|
||||
Dll.cpp
|
||||
loadxisf.cpp
|
||||
loadimage.cpp
|
||||
TenmonThumbnailProvider.cpp
|
||||
../rawimage.h
|
||||
../rawimage.cpp
|
||||
@@ -13,19 +13,19 @@ if(BUILD_THUMBNAILER)
|
||||
|
||||
target_compile_definitions(tenmonthumbnailer PRIVATE NO_QT)
|
||||
target_include_directories(tenmonthumbnailer PRIVATE ../libXISF)
|
||||
target_link_libraries(tenmonthumbnailer PRIVATE shlwapi ${LCMS2_LIB} ${FITS_LIB} XISF)
|
||||
target_link_libraries(tenmonthumbnailer PRIVATE shlwapi ${FITS_LIB} XISF)
|
||||
target_link_options(tenmonthumbnailer PRIVATE "-static")
|
||||
else(WIN32)
|
||||
qt_add_executable(tenmonthumbnailer
|
||||
main.cpp
|
||||
../loadimage.cpp
|
||||
loadimage.cpp
|
||||
../rawimage.cpp
|
||||
../rawimage_sse.cpp
|
||||
../imageinfodata.cpp)
|
||||
../rawimage_sse.cpp)
|
||||
|
||||
target_link_libraries(tenmonthumbnailer PRIVATE Qt6::Core Qt6::Gui ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF)
|
||||
target_link_libraries(tenmonthumbnailer PRIVATE ${FITS_LIB} XISF)
|
||||
|
||||
target_include_directories(tenmonthumbnailer PRIVATE ../libXISF)
|
||||
target_compile_definitions(tenmonthumbnailer PRIVATE NO_QT)
|
||||
endif(WIN32)
|
||||
endif(BUILD_THUMBNAILER)
|
||||
|
||||
|
||||
@@ -2,9 +2,87 @@
|
||||
#include <thumbcache.h> // For IThumbnailProvider.
|
||||
#include <new>
|
||||
#include "libxisf.h"
|
||||
#include "../rawimage.h"
|
||||
|
||||
bool loadXISF(const LibXISF::ByteArray &data, HBITMAP *hbmp, UINT thumbSize);
|
||||
bool loadFITS(const LibXISF::ByteArray &data, HBITMAP *hbmp, UINT thumbSize);
|
||||
bool loadXISF(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage);
|
||||
bool loadFITS(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage);
|
||||
|
||||
void RawImageToHTBITMAP(std::shared_ptr<RawImage> &rawImage, HBITMAP *hbmp, UINT thumbSize)
|
||||
{
|
||||
rawImage->calcStats();
|
||||
|
||||
DWORD thre = 20;
|
||||
DWORD dataSize = 4;
|
||||
HRESULT hr = HRESULT_FROM_WIN32(RegGetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\nou\\Tenmon\\settings", L"thumbnailstretchthreshold", RRF_RT_DWORD, NULL, &thre, &dataSize));
|
||||
|
||||
float thref = 0.1f;
|
||||
if(hr == S_OK)
|
||||
thref = thre / 100.0f;
|
||||
|
||||
if(rawImage->imageStats().m_median[0] < rawImage->norm() * thref)
|
||||
{
|
||||
//OutputDebugStringA("Stretch image");
|
||||
MTFParam params = rawImage->calcMTFParams();
|
||||
rawImage->applySTF(params);
|
||||
}
|
||||
|
||||
UINT w = rawImage->width();
|
||||
UINT h = rawImage->height();
|
||||
|
||||
UINT cw = thumbSize;
|
||||
UINT ch = thumbSize;
|
||||
if (w > h)
|
||||
ch = h * thumbSize / w;
|
||||
else
|
||||
cw = w * thumbSize / h;
|
||||
|
||||
|
||||
rawImage->resize(cw, ch);
|
||||
rawImage->convertToType(RawImage::UINT8);
|
||||
|
||||
BITMAPINFO bmi = {};
|
||||
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
|
||||
bmi.bmiHeader.biWidth = cw;
|
||||
bmi.bmiHeader.biHeight = -static_cast<LONG>(ch);
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
UINT lw = cw * 4;
|
||||
|
||||
BYTE *pBits;
|
||||
HBITMAP bmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, reinterpret_cast<void **>(&pBits), NULL, 0);
|
||||
|
||||
const unsigned char *p = (const unsigned char*)rawImage->data();
|
||||
const unsigned short *ps = (const unsigned short*)rawImage->data();
|
||||
if(rawImage->channels() == 1)
|
||||
{
|
||||
for(UINT y = 0; y < ch; y++)
|
||||
{
|
||||
for(UINT x = 0; x < cw; x++)
|
||||
{
|
||||
pBits[(y * lw) + x * 4 + 0] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 1] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 2] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(UINT y = 0; y < ch; y++)
|
||||
{
|
||||
for(UINT x = 0; x < cw; x++)
|
||||
{
|
||||
pBits[(y * lw) + x * 4 + 0] = p[y * cw * 4 + x * 4 + 2];
|
||||
pBits[(y * lw) + x * 4 + 1] = p[y * cw * 4 + x * 4 + 1];
|
||||
pBits[(y * lw) + x * 4 + 2] = p[y * cw * 4 + x * 4 + 0];
|
||||
pBits[(y * lw) + x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*hbmp = bmp;
|
||||
}
|
||||
|
||||
class TenmonThumbProvider : public IInitializeWithStream,
|
||||
public IThumbnailProvider
|
||||
@@ -103,19 +181,19 @@ IFACEMETHODIMP TenmonThumbProvider::GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_AL
|
||||
*pdwAlpha = WTSAT_RGB;
|
||||
|
||||
data.resize(readSize);
|
||||
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(data[0] == 'X' && data[1] == 'I' && data[2] == 'S' && data[3] == 'F')
|
||||
{
|
||||
if(loadXISF(data, phbmp, cx))
|
||||
return S_OK;
|
||||
else
|
||||
if(!loadXISF(data, rawImage))
|
||||
return E_FAIL;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(loadFITS(data, phbmp, cx))
|
||||
return S_OK;
|
||||
else
|
||||
if(!loadFITS(data, rawImage))
|
||||
return E_FAIL;
|
||||
}
|
||||
return E_FAIL;
|
||||
|
||||
RawImageToHTBITMAP(rawImage, phbmp, cx);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ int generateThumbnail(const QString &input, const QString &output, uint32_t size
|
||||
{
|
||||
ImageInfoData info;
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(!loadImage(input, info, rawImage))
|
||||
if(!loadImage(input, info, rawImage, 0))
|
||||
return 2;
|
||||
|
||||
if(!rawImage)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "libxisf.h"
|
||||
#include "../rawimage.h"
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <fitsio2.h>
|
||||
|
||||
bool OpenGLES = false;
|
||||
|
||||
bool loadXISF(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(data);
|
||||
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(0);
|
||||
|
||||
RawImage::DataType type;
|
||||
switch(xisfImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
|
||||
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
|
||||
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
|
||||
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
|
||||
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
LibXISF::Image tmpImage = xisfImage;
|
||||
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
|
||||
|
||||
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
|
||||
{
|
||||
rawImage = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
|
||||
std::memcpy(rawImage->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
|
||||
}
|
||||
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
|
||||
{
|
||||
rawImage = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
#ifdef WIN32
|
||||
char text[1024];
|
||||
sprintf_s(text, 1000, "Failed to open XISF image %s", err.what());
|
||||
OutputDebugStringA(text);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loadFITS(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage)
|
||||
{
|
||||
fitsfile *file;
|
||||
|
||||
int status = 0;
|
||||
int hdutype = -1;
|
||||
int num = 0;
|
||||
long naxes[3] = {0};
|
||||
|
||||
auto checkError = [&status]()
|
||||
{
|
||||
char err[100];
|
||||
fits_get_errstatus(status, err);
|
||||
#ifdef WIN32
|
||||
char text[1000];
|
||||
sprintf_s(text, 1000, "Failed to load FITS file %s", err);
|
||||
OutputDebugStringA(text);
|
||||
#endif
|
||||
return false;
|
||||
};
|
||||
|
||||
const void *dataPtr = data.data();
|
||||
size_t size = data.size();
|
||||
fits_open_memfile(&file, "file.fits", READONLY, (void**)&dataPtr, &size, 0, nullptr, &status);
|
||||
if(status)return checkError();
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
if(status)return checkError();
|
||||
|
||||
int imgtype;
|
||||
int naxis;
|
||||
for(int i=1; i <= num; i++)
|
||||
{
|
||||
fits_movabs_hdu(file, i, &hdutype, &status);if(status)return checkError();
|
||||
if(hdutype == IMAGE_HDU)
|
||||
{
|
||||
naxes[0] = naxes[1] = naxes[2] = 0;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();
|
||||
|
||||
if(hdutype == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
{
|
||||
RawImage::DataType type;
|
||||
int fitstype;
|
||||
long fpixel[3] = {1,1,1};
|
||||
switch(imgtype)
|
||||
{
|
||||
case BYTE_IMG:
|
||||
type = RawImage::UINT8;
|
||||
fitstype = TBYTE;
|
||||
break;
|
||||
case SHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TSHORT;
|
||||
break;
|
||||
case USHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case ULONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TUINT;
|
||||
break;
|
||||
case FLOAT_IMG:
|
||||
type = RawImage::FLOAT32;
|
||||
fitstype = TFLOAT;
|
||||
break;
|
||||
case DOUBLE_IMG:
|
||||
type = RawImage::FLOAT64;
|
||||
fitstype = TDOUBLE;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t size = naxes[0]*naxes[1];
|
||||
size_t w = naxes[0];
|
||||
size_t h = naxes[1];
|
||||
|
||||
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
|
||||
uint8_t *data = static_cast<uint8_t*>(img.data());
|
||||
for (int i=1; i==1 || i<=naxes[2]; i++)
|
||||
{
|
||||
fpixel[2] = i;
|
||||
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
|
||||
if(status)return checkError();
|
||||
}
|
||||
if(fitstype == TSHORT)
|
||||
{
|
||||
uint16_t *s = static_cast<uint16_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT16_MIN;
|
||||
}
|
||||
|
||||
if(img.channels() == 1)
|
||||
rawImage = std::make_shared<RawImage>(std::move(img));
|
||||
else
|
||||
rawImage = RawImage::fromPlanar(img);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
#include "libxisf.h"
|
||||
#include <thumbcache.h>
|
||||
#include "../rawimage.h"
|
||||
#include <fitsio2.h>
|
||||
|
||||
bool OpenGLES = false;
|
||||
|
||||
void RawImageToHTBITMAP(std::shared_ptr<RawImage> &rawImage, HBITMAP *hbmp, UINT thumbSize)
|
||||
{
|
||||
rawImage->calcStats();
|
||||
|
||||
DWORD thre = 10;
|
||||
DWORD dataSize = 4;
|
||||
//HRESULT hr = HRESULT_FROM_WIN32(RegGetValueW(HKEY_CURRENT_USER, L"SOFTWARE\\nou\\Tenmon\\settings", L"thumbnailstretchthreshold", RRF_RT_DWORD, NULL, &thre, &dataSize));
|
||||
|
||||
float thref = 0.1f;
|
||||
/*if(hr == S_OK)
|
||||
thref = thre / 100.0f;*/
|
||||
|
||||
if(rawImage->imageStats().m_mean[0] < rawImage->norm() * thref)
|
||||
{
|
||||
//OutputDebugStringA("Stretch image");
|
||||
MTFParam params = rawImage->calcMTFParams();
|
||||
rawImage->applySTF(params);
|
||||
}
|
||||
|
||||
UINT w = rawImage->width();
|
||||
UINT h = rawImage->height();
|
||||
|
||||
UINT cw = thumbSize;
|
||||
UINT ch = thumbSize;
|
||||
if (w > h)
|
||||
ch = h * thumbSize / w;
|
||||
else
|
||||
cw = w * thumbSize / h;
|
||||
|
||||
|
||||
rawImage->resize(cw, ch);
|
||||
rawImage->convertToType(RawImage::UINT8);
|
||||
|
||||
BITMAPINFO bmi = {};
|
||||
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
|
||||
bmi.bmiHeader.biWidth = cw;
|
||||
bmi.bmiHeader.biHeight = -static_cast<LONG>(ch);
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
UINT lw = cw * 4;
|
||||
|
||||
BYTE *pBits;
|
||||
HBITMAP bmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, reinterpret_cast<void **>(&pBits), NULL, 0);
|
||||
|
||||
const unsigned char *p = (const unsigned char*)rawImage->data();
|
||||
const unsigned short *ps = (const unsigned short*)rawImage->data();
|
||||
if(rawImage->channels() == 1)
|
||||
{
|
||||
for(UINT y = 0; y < ch; y++)
|
||||
{
|
||||
for(UINT x = 0; x < cw; x++)
|
||||
{
|
||||
pBits[(y * lw) + x * 4 + 0] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 1] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 2] = p[y * cw + x];
|
||||
pBits[(y * lw) + x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(UINT y = 0; y < ch; y++)
|
||||
{
|
||||
for(UINT x = 0; x < cw; x++)
|
||||
{
|
||||
pBits[(y * lw) + x * 4 + 0] = p[y * cw * 4 + x * 4 + 2];
|
||||
pBits[(y * lw) + x * 4 + 1] = p[y * cw * 4 + x * 4 + 1];
|
||||
pBits[(y * lw) + x * 4 + 2] = p[y * cw * 4 + x * 4 + 0];
|
||||
pBits[(y * lw) + x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*hbmp = bmp;
|
||||
}
|
||||
|
||||
bool loadXISF(const LibXISF::ByteArray &data, HBITMAP *hbmp, UINT thumbSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(data);
|
||||
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(0);
|
||||
|
||||
RawImage::DataType type;
|
||||
switch(xisfImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
|
||||
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
|
||||
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
|
||||
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
|
||||
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
LibXISF::Image tmpImage = xisfImage;
|
||||
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
|
||||
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
|
||||
{
|
||||
rawImage = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
|
||||
std::memcpy(rawImage->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
|
||||
}
|
||||
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
|
||||
{
|
||||
rawImage = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
|
||||
}
|
||||
|
||||
RawImageToHTBITMAP(rawImage, hbmp, thumbSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
char text[1024];
|
||||
sprintf_s(text, 1000, "Failed to open XISF image %s", err.what());
|
||||
OutputDebugStringA(text);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loadFITS(const LibXISF::ByteArray &data, HBITMAP *hbmp, UINT thumbSize)
|
||||
{
|
||||
fitsfile *file;
|
||||
|
||||
int status = 0;
|
||||
int type = -1;
|
||||
int num = 0;
|
||||
long naxes[3] = {0};
|
||||
|
||||
auto checkError = [&status]()
|
||||
{
|
||||
char err[100];
|
||||
char text[1000];
|
||||
fits_get_errstatus(status, err);
|
||||
sprintf_s(text, 1000, "Failed to load FITS file %s", err);
|
||||
OutputDebugStringA(text);
|
||||
return false;
|
||||
};
|
||||
|
||||
const void *dataPtr = data.data();
|
||||
size_t size = data.size();
|
||||
fits_open_memfile(&file, "file.fits", READONLY, (void**)&dataPtr, &size, 0, nullptr, &status);
|
||||
if(status)return checkError();
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
if(status)return checkError();
|
||||
|
||||
int imgtype;
|
||||
int naxis;
|
||||
for(int i=1; i <= num; i++)
|
||||
{
|
||||
fits_movabs_hdu(file, i, IMAGE_HDU, &status);if(status)return checkError();
|
||||
fits_get_hdu_type(file, &type, &status);if(status)return checkError();
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);if(status)return checkError();
|
||||
fits_get_img_equivtype(file, &imgtype, &status);if(status)return checkError();
|
||||
|
||||
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
{
|
||||
RawImage::DataType type;
|
||||
int fitstype;
|
||||
long fpixel[3] = {1,1,1};
|
||||
switch(imgtype)
|
||||
{
|
||||
case BYTE_IMG:
|
||||
type = RawImage::UINT8;
|
||||
fitstype = TBYTE;
|
||||
break;
|
||||
case SHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TSHORT;
|
||||
break;
|
||||
case USHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case ULONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TUINT;
|
||||
break;
|
||||
case FLOAT_IMG:
|
||||
type = RawImage::FLOAT32;
|
||||
fitstype = TFLOAT;
|
||||
break;
|
||||
case DOUBLE_IMG:
|
||||
type = RawImage::FLOAT64;
|
||||
fitstype = TDOUBLE;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t size = naxes[0]*naxes[1];
|
||||
size_t w = naxes[0];
|
||||
size_t h = naxes[1];
|
||||
|
||||
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
|
||||
uint8_t *data = static_cast<uint8_t*>(img.data());
|
||||
for (int i=1; i==1 || i<=naxes[2]; i++)
|
||||
{
|
||||
fpixel[2] = i;
|
||||
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
|
||||
if(status)return checkError();
|
||||
}
|
||||
if(fitstype == TSHORT)
|
||||
{
|
||||
uint16_t *s = static_cast<uint16_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT16_MIN;
|
||||
}
|
||||
|
||||
std::shared_ptr<RawImage> image;
|
||||
if(img.channels() == 1)
|
||||
image = std::make_shared<RawImage>(std::move(img));
|
||||
else
|
||||
image = RawImage::fromPlanar(img);
|
||||
|
||||
RawImageToHTBITMAP(image, hbmp, thumbSize);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
+53
-35
@@ -1,62 +1,80 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "../rawimage.h"
|
||||
#include "../loadimage.h"
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
|
||||
bool OpenGLES = false;
|
||||
bool loadXISF(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage);
|
||||
bool loadFITS(const LibXISF::ByteArray &data, std::shared_ptr<RawImage> &rawImage);
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
std::vector<std::string> args;
|
||||
for(int i=0; i<argc; i++)
|
||||
args.push_back(argv[i]);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.addOption({{"s", "size"}, "Size of the thumbnail in pixels (default: 128)", "size", "128"});
|
||||
parser.addPositionalArgument("input", "Input image file");
|
||||
parser.addPositionalArgument("output", "Output image file");
|
||||
parser.addHelpOption();
|
||||
|
||||
parser.process(a);
|
||||
|
||||
QStringList args = parser.positionalArguments();
|
||||
|
||||
if(args.size() < 2)
|
||||
if(args.size() < 3)
|
||||
return 1;
|
||||
|
||||
QString input = args[0];
|
||||
QString output = args[1];
|
||||
std::string input = args[1];
|
||||
std::string output = args[2];
|
||||
|
||||
ImageInfoData info;
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(!loadImage(input, info, rawImage))
|
||||
|
||||
LibXISF::ByteArray data;
|
||||
std::ifstream fr;
|
||||
fr.open(input, std::ios_base::in | std::ios_base::binary);
|
||||
if(!fr.is_open())
|
||||
return 2;
|
||||
|
||||
if(!rawImage)
|
||||
fr.seekg(0, std::ios_base::end);
|
||||
size_t len = fr.tellg();
|
||||
fr.seekg(0, std::ios_base::beg);
|
||||
data.resize(len);
|
||||
fr.read(data.data(), len);
|
||||
if(fr.bad())
|
||||
return 3;
|
||||
|
||||
bool ok;
|
||||
int size = parser.value("s").toInt(&ok);
|
||||
if(!ok)
|
||||
size = 128;
|
||||
if(input.find(".xisf") != std::string::npos)
|
||||
{
|
||||
if(!loadXISF(data, rawImage))
|
||||
return 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!loadFITS(data, rawImage))
|
||||
return 4;
|
||||
}
|
||||
|
||||
if(!rawImage)
|
||||
return 5;
|
||||
|
||||
uint32_t thumbSize = 256;
|
||||
|
||||
uint32_t w = rawImage->width();
|
||||
uint32_t h = rawImage->height();
|
||||
|
||||
uint32_t cw = thumbSize;
|
||||
uint32_t ch = thumbSize;
|
||||
if (w > h)
|
||||
ch = h * thumbSize / w;
|
||||
else
|
||||
cw = w * thumbSize / h;
|
||||
|
||||
QSize rect(rawImage->width(), rawImage->height());
|
||||
rect.scale(size, size, Qt::KeepAspectRatio);
|
||||
rawImage->calcStats();
|
||||
rawImage->resize(rect.width(), rect.height());
|
||||
if(rawImage->imageStats().m_median[0] < rawImage->norm() * 0.2f)
|
||||
rawImage->resize(cw, ch);
|
||||
if(rawImage->imageStats().m_median[0] < rawImage->norm() * 0.1f)
|
||||
{
|
||||
MTFParam mtfParams = rawImage->calcMTFParams(true);
|
||||
rawImage->applySTF(mtfParams);
|
||||
}
|
||||
rawImage->convertToType(RawImage::UINT8);
|
||||
|
||||
QImage img;
|
||||
if(rawImage->channels() == 1)
|
||||
img = QImage((const uchar*)rawImage->data(), rawImage->width(), rawImage->height(), rawImage->widthBytes(), QImage::Format_Grayscale8);
|
||||
stbi_write_png(output.c_str(), cw, ch, 1, rawImage->data(), rawImage->widthBytes());
|
||||
else
|
||||
img = QImage((const uchar*)rawImage->data(), rawImage->width(), rawImage->height(), rawImage->widthBytes(), QImage::Format_RGBA8888);
|
||||
|
||||
if(!img.save(output, "png"))
|
||||
return 4;
|
||||
stbi_write_png(output.c_str(), cw, ch, 4, rawImage->data(), rawImage->widthBytes());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+366
-162
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+382
-178
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+370
-166
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+367
-163
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user