Compare commits
18 Commits
90026f931f
...
20250429
| Author | SHA1 | Date | |
|---|---|---|---|
| c6bc792ff7 | |||
| 1a307d82f9 | |||
| 8c5e2b2ebf | |||
| 03ad135ef0 | |||
| 2a78a9a41d | |||
| 1a214a169e | |||
| f8704c51d8 | |||
| 3feee0256c | |||
| 53472d807c | |||
| 9f06269aa4 | |||
| 78f242d808 | |||
| e6bab45a89 | |||
| 58286d52c5 | |||
| bac1963fa4 | |||
| 2415717ce0 | |||
| e7acbca01e | |||
| 7c4118b0b6 | |||
| 8178efdafd |
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
+40
-21
@@ -14,6 +14,7 @@
|
||||
#include <QChartView>
|
||||
#include <QLineSeries>
|
||||
#include "scriptengine.h"
|
||||
#include "chartgraph.h"
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QCloseEvent>
|
||||
@@ -271,31 +272,49 @@ QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
|
||||
void BatchProcessing::plot(const QVector<QPointF> &points)
|
||||
QJSValue BatchProcessing::question(const QString &question, const QStringList &buttons, const QString &title)
|
||||
{
|
||||
QDialog *diag = new QDialog(this);
|
||||
diag->setAttribute(Qt::WA_DeleteOnClose);
|
||||
diag->setModal(false);
|
||||
diag->setWindowTitle(tr("Chart"));
|
||||
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;
|
||||
|
||||
QChartView *chartView = new QChartView(diag);
|
||||
diag->setLayout(new QVBoxLayout);
|
||||
diag->layout()->addWidget(chartView);
|
||||
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;
|
||||
}
|
||||
|
||||
QChart *chart = new QChart;
|
||||
chart->setParent(chartView);
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto series = new QLineSeries(chartView);
|
||||
series->append(points);
|
||||
chart->addSeries(series);
|
||||
chart->createDefaultAxes();
|
||||
chart->setTitle("Simple line graph");
|
||||
chart->legend()->hide();
|
||||
|
||||
chartView->setChart(chart);
|
||||
chartView->setRenderHint(QPainter::Antialiasing);
|
||||
diag->resize(640, 480);
|
||||
diag->show();
|
||||
void BatchProcessing::plot(const QVariant &graph)
|
||||
{
|
||||
ChartGraph *chart = new ChartGraph(this);
|
||||
chart->plot(graph);
|
||||
}
|
||||
|
||||
void openDir(const QString &path)
|
||||
|
||||
+2
-1
@@ -41,8 +41,9 @@ 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 QVector<QPointF> &points);
|
||||
void plot(const QVariant &graph);
|
||||
};
|
||||
|
||||
void openDir(const QString &path);
|
||||
|
||||
+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
|
||||
+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);
|
||||
}
|
||||
|
||||
@@ -294,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;
|
||||
|
||||
@@ -109,6 +109,7 @@ public slots:
|
||||
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);
|
||||
|
||||
@@ -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})
|
||||
|
||||
+1
-1
@@ -248,7 +248,7 @@ bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage
|
||||
}
|
||||
|
||||
info.num = xisf.imagesCount();
|
||||
info.index = index + 1;
|
||||
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())});
|
||||
|
||||
+3
-1
@@ -166,6 +166,7 @@ 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));
|
||||
|
||||
@@ -251,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());
|
||||
@@ -262,6 +262,8 @@ 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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+89
-11
@@ -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,18 +136,38 @@ QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, i
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ScriptEngine::plot(const QJSValue &pointsArray)
|
||||
QJSValue ScriptEngine::question(const QString &question, const QStringList &buttons, const QString &title) const
|
||||
{
|
||||
if(pointsArray.isArray())
|
||||
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))
|
||||
{
|
||||
int len = pointsArray.property("length").toInt();
|
||||
QVector<QPointF> points;
|
||||
for(int i = 0; i < len; i++)
|
||||
{
|
||||
QJSValue point = pointsArray.property(i);
|
||||
points.append(QPointF(point.property("x").toNumber(), point.property("y").toNumber()));
|
||||
}
|
||||
QMetaObject::invokeMethod(_parent, "plot", Qt::QueuedConnection, Q_ARG(QVector<QPointF>, points));
|
||||
return _jsEngine->newQObject(textFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
logError("Failed to open file " + fileName);
|
||||
delete textFile;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,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())
|
||||
{
|
||||
@@ -835,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-1
@@ -8,7 +8,7 @@
|
||||
#include <QThreadPool>
|
||||
#include <QSemaphore>
|
||||
#include "database.h"
|
||||
#include "imageinfo.h"
|
||||
#include "imageinfodata.h"
|
||||
|
||||
class BatchProcessing;
|
||||
class Solver;
|
||||
@@ -47,7 +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);
|
||||
@@ -143,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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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