Compare commits
14 Commits
2415717ce0
...
20250429
| Author | SHA1 | Date | |
|---|---|---|---|
| c6bc792ff7 | |||
| 1a307d82f9 | |||
| 8c5e2b2ebf | |||
| 03ad135ef0 | |||
| 2a78a9a41d | |||
| 1a214a169e | |||
| f8704c51d8 | |||
| 3feee0256c | |||
| 53472d807c | |||
| 9f06269aa4 | |||
| 78f242d808 | |||
| e6bab45a89 | |||
| 58286d52c5 | |||
| bac1963fa4 |
@@ -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
|
||||
@@ -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())});
|
||||
|
||||
+2
-1
@@ -252,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());
|
||||
@@ -263,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);
|
||||
|
||||
+87
-9
@@ -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()));
|
||||
return _jsEngine->newQObject(textFile);
|
||||
}
|
||||
QMetaObject::invokeMethod(_parent, "plot", Qt::QueuedConnection, Q_ARG(QVector<QPointF>, points));
|
||||
else
|
||||
{
|
||||
logError("Failed to open file " + fileName);
|
||||
delete textFile;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+3
-1
@@ -89,8 +89,8 @@ 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);
|
||||
@@ -98,6 +98,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
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);
|
||||
@@ -136,6 +137,7 @@ 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);
|
||||
|
||||
@@ -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>
|
||||
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