LibreVNA/Software/PC_Application/LibreVNA-GUI/Traces/tracewidget.cpp
2025-06-23 16:39:18 +02:00

661 lines
23 KiB
C++

#include "tracewidget.h"
#include "ui_tracewidget.h"
#include "traceeditdialog.h"
#include "traceimportdialog.h"
#include "ui_s2pImportOptions.h"
#include "CustomWidgets/informationbox.h"
#include "tracetouchstoneexport.h"
#include "trace.h"
#include "unit.h"
#include "Util/util.h"
#include "appwindow.h"
#include <QKeyEvent>
#include <QFileDialog>
#include <QDrag>
#include <QMimeData>
#include <QDebug>
#include <QMenu>
#include <QTableView>
#include <QDebug>
TraceWidget::TraceWidget(TraceModel &model, Calibration *cal, Deembedding *deembed, QWidget *parent) :
QWidget(parent),
SCPINode("TRACe"),
dragTrace(nullptr),
ui(new Ui::TraceWidget),
model(model),
dropFilename(""),
cal(cal),
deembed(deembed)
{
ui->setupUi(this);
ui->view->setModel(&model);
ui->view->setAutoScroll(false);
ui->view->viewport()->installEventFilter(this);
connect(ui->bImport, &QPushButton::clicked, this, &TraceWidget::importDialog);
connect(ui->bExport, &QPushButton::clicked, this, &TraceWidget::exportDialog);
connect(ui->view->selectionModel(), &QItemSelectionModel::currentRowChanged, this, [=](const QModelIndex &current, const QModelIndex &previous){
Q_UNUSED(previous)
ui->edit->setEnabled(current.isValid());
ui->remove->setEnabled(current.isValid());
});
installEventFilter(this);
setAcceptDrops(true);
createCount = 0;
SetupSCPI();
}
TraceWidget::~TraceWidget()
{
delete ui;
}
void TraceWidget::on_add_clicked()
{
createCount++;
auto t = new Trace("Trace #"+QString::number(createCount), Qt::darkYellow, defaultParameter());
t->setColor(QColor::fromHsl((createCount * 50) % 360, 250, 128));
model.addTrace(t);
ui->view->selectRow(model.getTraces().size() - 1);
on_edit_clicked();
}
void TraceWidget::on_remove_clicked()
{
QModelIndex index = ui->view->currentIndex();
if (index.isValid()) { // if nothing clicked, index.row() = -1
model.removeTrace(index.row());
// otherwise, TraceModel casts index to unsigned int and compares with traces.size() which is int
};
}
bool TraceWidget::eventFilter(QObject *, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
int key = static_cast<QKeyEvent *>(event)->key();
if(key == Qt::Key_Escape) {
ui->view->clearSelection();
ui->edit->setEnabled(false);
ui->remove->setEnabled(false);
return true;
} else if(key == Qt::Key_Delete) {
model.removeTrace(ui->view->currentIndex().row());
return true;
}
} else if(event->type() == QEvent::MouseButtonPress) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
auto index = ui->view->indexAt(mouseEvent->pos());
if(index.isValid()) {
dragStartPosition = mouseEvent->pos();
dragTrace = model.trace(index.row());
} else {
dragTrace = nullptr;
}
}
return false;
} else if(event->type() == QEvent::MouseMove) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (!(mouseEvent->buttons() & Qt::LeftButton)) {
return false;
}
if (!dragTrace) {
return false;
}
if ((mouseEvent->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance()) {
return false;
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
QByteArray encodedPointer;
QDataStream stream(&encodedPointer, QIODevice::WriteOnly);
stream << quintptr(dragTrace);
mimeData->setData("trace/pointer", encodedPointer);
drag->setMimeData(mimeData);
drag->exec(Qt::CopyAction);
return true;
}
return false;
}
void TraceWidget::dragEnterEvent(QDragEnterEvent *event)
{
QString data = "";
if(event->mimeData()->hasFormat("text/plain")) {
data = QString(event->mimeData()->data("text/plain"));
if (data.startsWith("file://")) {
data = data.trimmed();
data.remove(0, 7);
} else {
// something else
data = "";
}
} else if(event->mimeData()->hasFormat("text/uri-list")) {
data = QString(event->mimeData()->data("text/uri-list"));
if (data.startsWith("file:///")) {
data = data.trimmed();
data.remove(0, 8);
} else {
// something else
data = "";
}
}
if(!data.isEmpty()) {
// extract file path/name and type
if(data.contains(".")) {
auto type = data.split(".").last();
if (supportsImportFileFormats().contains(type)) {
dropFilename = data;
qDebug() << "prepared to drop file " << dropFilename;
event->accept();
return;
}
}
}
event->ignore();
dropFilename = "";
}
void TraceWidget::dropEvent(QDropEvent *event)
{
Q_UNUSED(event);
if(dropFilename.size() > 0) {
if(importFile(dropFilename)) {
event->accept();
}
}
dropFilename = "";
}
void TraceWidget::on_edit_clicked()
{
if(ui->view->currentIndex().isValid()) {
auto edit = new TraceEditDialog(*model.trace(ui->view->currentIndex().row()));
if(AppWindow::showGUI()) {
edit->show();
}
}
}
void TraceWidget::on_view_doubleClicked(const QModelIndex &index)
{
if(index.column() == TraceModel::ColIndexName) {
auto edit = new TraceEditDialog(*model.trace(index.row()));
if(AppWindow::showGUI()) {
edit->show();
}
}
}
void TraceWidget::on_view_clicked(const QModelIndex &index)
{
switch(index.column()) {
case TraceModel::ColIndexVisible:
model.toggleVisibility(index.row());
break;
case TraceModel::ColIndexPlayPause:
model.togglePause(index.row());
break;
case TraceModel::ColIndexDeembedding:
model.toggleDeembedding(index.row());
break;
case TraceModel::ColIndexMath:
model.toggleMath(index.row());
break;
default:
break;
}
}
void TraceWidget::importDialog()
{
QString supported = "Supported files (";
for(auto f : supportsImportFileFormats()) {
supported += "*."+f+" ";
}
supported.chop(1);
supported += ")";
auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", Preferences::getInstance().UISettings.Paths.data, supported, nullptr, Preferences::QFileDialogOptions());
if (!filename.isEmpty()) {
Preferences::getInstance().UISettings.Paths.data = QFileInfo(filename).path();
importFile(filename);
}
}
bool TraceWidget::importFile(QString filename)
{
if(!filename.contains(".")) {
// no ending, not supported
return false;
}
auto format = filename.split(".").last();
if(!supportsImportFileFormats().contains(format)) {
// unsupported format
return false;
}
// try to import the file
try {
std::vector<Trace*> traces;
int touchstonePorts = 0;
QString prefix = QString();
if(filename.endsWith(".csv")) {
auto csv = CSV::fromFile(filename);
traces = Trace::createFromCSV(csv);
} else {
// must be a touchstone file
auto t = Touchstone::fromFile(filename.toStdString());
traces = Trace::createFromTouchstone(t);
touchstonePorts = t.ports();
}
// contruct prefix from filename
prefix = filename;
// remove any directory names (keep only the filename itself)
int lastSlash = qMax(prefix.lastIndexOf('/'), prefix.lastIndexOf('\\'));
if(lastSlash != -1) {
prefix.remove(0, lastSlash + 1);
}
// remove file type
prefix.truncate(prefix.indexOf('.'));
prefix.append("_");
auto i = new TraceImportDialog(model, traces, prefix);
if(AppWindow::showGUI()) {
i->show();
}
// potential candidate to process via calibration/de-embedding
connect(i, &TraceImportDialog::importFinsished, [=](const std::vector<Trace*> &traces) {
if(traces.size() == touchstonePorts*touchstonePorts) {
// all traces imported, can calculate calibration/de-embedding
bool calAvailable = cal && cal->getNumPoints() > 0;
bool deembedAvailable = deembed && deembed->getOptions().size() > 0;
if(calAvailable || deembedAvailable) {
// check if user wants to apply either one to the imported traces
auto dialog = new QDialog();
auto ui = new Ui::s2pImportOptions;
ui->setupUi(dialog);
connect(dialog, &QDialog::finished, [=](){
delete ui;
});
ui->applyCal->setEnabled(calAvailable);
ui->deembed->setEnabled(deembedAvailable);
bool applyCal = false;
bool applyDeembed = false;
connect(ui->applyCal, &QCheckBox::toggled, [&](bool checked) {
applyCal = checked;
});
connect(ui->deembed, &QCheckBox::toggled, [&](bool checked) {
applyDeembed = checked;
});
if(AppWindow::showGUI()) {
dialog->exec();
}
// assemble trace set
std::map<QString, Trace*> set;
for(int i=1;i<=touchstonePorts;i++) {
for(int j=1;j<=touchstonePorts;j++) {
QString name = "S"+QString::number(i)+QString::number(j);
int index = (i-1)*touchstonePorts+(j-1);
set[name] = traces[index];
}
}
if(applyCal) {
cal->correctTraces(set);
}
if(applyDeembed) {
deembed->Deembed(set);
}
}
}
});
return true;
} catch(const std::exception& e) {
InformationBox::ShowError("Failed to import file", QString("Attempt to import file ended with error: \"") + e.what()+"\"");
return false;
}
}
void TraceWidget::SetupSCPI()
{
auto findTraceFromName = [=](QString name) -> Trace* {
// check if trace is specified by number
bool ok;
auto n = name.toUInt(&ok);
if(ok) {
// check if enough traces exist
if(n < model.getTraces().size()) {
return model.getTraces()[n];
} else {
// invalid number
return nullptr;
}
} else {
// trace specified by name
for(auto t : model.getTraces()) {
if(t->name().compare(name, Qt::CaseInsensitive) == 0) {
return t;
}
}
// not found
return nullptr;
}
};
auto findTrace = [=](QStringList params) -> Trace* {
if(params.size() < 1) {
return nullptr;
}
return findTraceFromName(params[0]);
};
auto createStringFromData = [](Trace *t, const Trace::Data &d) -> QString {
if(Trace::isSAParameter(t->liveParameter())) {
if(std::isnan(d.x)) {
return "NaN";
}
return QString::number(Util::SparamTodB(d.y.real()));
} else {
if(std::isnan(d.x)) {
return "NaN,NaN";
}
return QString::number(d.y.real())+","+QString::number(d.y.imag());
}
};
add(new SCPICommand("LIST", nullptr, [=](QStringList){
QString ret;
for(auto t : model.getTraces()) {
ret += t->name() + ",";
}
ret.chop(1);
return ret;
}));
add(new SCPICommand("DATA", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
QString ret;
if(t->size() > 0) {
for(unsigned int i=0;i<t->size();i++) {
auto d = t->sample(i);
int precision = 0;
switch(t->outputType()) {
case Trace::DataType::Invalid:
case Trace::DataType::Frequency: precision = 0; break;
case Trace::DataType::Time: precision = 12; break;
case Trace::DataType::Power: precision = 3; break;
case Trace::DataType::TimeZeroSpan: precision = 4; break;
}
ret += "[" + QString::number(d.x, 'f', precision) + ","+createStringFromData(t, d)+"],";
}
ret.chop(1);
} else {
ret = "EMPTY";
}
return ret;
}));
add(new SCPICommand("AT", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
double x;
if(!SCPI::paramToDouble(params, 1, x)) {
return SCPI::getResultName(SCPI::Result::Error);
} else {
auto d = t->interpolatedSample(x);
if(std::isnan(d.x)) {
return "NaN,NaN";
} else {
return createStringFromData(t, d);
}
}
}));
add(new SCPICommand("TOUCHSTONE", nullptr, [=](QStringList params) -> QString {
if(params.size() < 1) {
// no traces given
return SCPI::getResultName(SCPI::Result::Error);
}
// check number of paramaters, must be a square number
int numTraces = params.size();
int ports = round(sqrt(numTraces));
if(ports * ports != numTraces) {
// invalid number of traces
return SCPI::getResultName(SCPI::Result::Error);
}
Trace* traces[numTraces];
for(int i=0;i<numTraces;i++) {
traces[i] = findTraceFromName(params[i]);
if(!traces[i]) {
// couldn't find that trace
return SCPI::getResultName(SCPI::Result::Error);
}
}
// check if trace selection is valid
auto npoints = traces[0]->size();
auto f_start = traces[0]->minX();
auto f_stop = traces[0]->maxX();
for(int i=0;i<ports;i++) {
for(int j=0;j<ports;j++) {
bool need_reflection = i==j;
auto t = traces[j+i*ports];
if(t->outputType() != Trace::DataType::Frequency) {
// invalid domain
return SCPI::getResultName(SCPI::Result::Error);
}
if(t->isReflection() != need_reflection) {
// invalid measurement at this position
return SCPI::getResultName(SCPI::Result::Error);
}
if((t->size() != npoints) || (t->minX() != f_start) || (t->maxX() != f_stop)) {
// frequency points are not identical
return SCPI::getResultName(SCPI::Result::Error);
}
}
}
// all traces checked, they are valid.
// Constructing touchstone
Touchstone t = Touchstone(ports);
for(unsigned int i=0;i<npoints;i++) {
Touchstone::Datapoint d;
d.frequency = traces[0]->getSample(i).x;
for(auto trace : traces) {
d.S.push_back(trace->getSample(i).y);
}
t.AddDatapoint(d);
}
// touchstone assembled, save to dummyfile
auto s = t.toString(Touchstone::Scale::GHz, Touchstone::Format::RealImaginary);
return QString::fromStdString(s.str());
}));
add(new SCPICommand("MAXFrequency", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return QString::number(t->maxX(), 'f', 0);
}));
add(new SCPICommand("MINFrequency", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return QString::number(t->minX(), 'f', 0);
}));
add(new SCPICommand("MAXAmplitude", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto d = t->interpolatedSample(t->findExtremum(true));
return QString::number(d.x, 'f', 0)+","+createStringFromData(t, d);
}));
add(new SCPICommand("MINAmplitude", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto d = t->interpolatedSample(t->findExtremum(false));
return QString::number(d.x, 'f', 0)+","+createStringFromData(t, d);
}));
add(new SCPICommand("NEW", [=](QStringList params) -> QString {
if(params.size() != 1) {
return SCPI::getResultName(SCPI::Result::Error);
}
createCount++;
auto t = new Trace(params[0], Qt::darkYellow, defaultParameter());
t->setColor(QColor::fromHsl((createCount * 50) % 360, 250, 128));
model.addTrace(t);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
add(new SCPICommand("DELete", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
model.removeTrace(t);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
add(new SCPICommand("RENAME", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
if(params.size() != 2) {
return SCPI::getResultName(SCPI::Result::Error);
}
t->setName(params[1]);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
add(new SCPICommand("PAUSE", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
t->pause();
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
add(new SCPICommand("RESUME", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
t->resume();
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
add(new SCPICommand("PAUSED", nullptr, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return t->isPaused() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
}));
auto deembed = new SCPINode("DEEMBedding");
deembed->add(new SCPICommand("ACTive", [=](QStringList params) -> QString {
Trace* t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
bool activate = false;
if(!SCPI::paramToBool(params, 1, activate)) {
return SCPI::getResultName(SCPI::Result::Error);
}
if(activate) {
if(!t->deembeddingAvailable()) {
return SCPI::getResultName(SCPI::Result::Error);
}
t->setDeembeddingActive(true);
} else {
t->setDeembeddingActive(false);
}
return SCPI::getResultName(SCPI::Result::Empty);
}, [=](QStringList params) -> QString {
Trace* t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return t->isDeembeddingActive() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
}));
deembed->add(new SCPICommand("AVAILable", nullptr, [=](QStringList params) -> QString {
Trace* t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return t->deembeddingAvailable() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
}));
add(deembed);
add(new SCPICommand("PARAMeter", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t || params.size() < 2) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto newparam = params[1];
if((Trace::isVNAParameter(t->liveParameter()) && Trace::isVNAParameter(newparam))
|| (Trace::isSAParameter(t->liveParameter()) && Trace::isSAParameter(newparam))) {
t->fromLivedata(t->liveType(), newparam);
return SCPI::getResultName(SCPI::Result::Empty);
} else {
return SCPI::getResultName(SCPI::Result::Error);
}
}, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return t->liveParameter();
}));
add(new SCPICommand("TYPE", [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t || params.size() < 2) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto newtype = Trace::TypeFromString(params[1]);
if(newtype != Trace::LivedataType::Invalid) {
t->fromLivedata(newtype, t->liveParameter());
return SCPI::getResultName(SCPI::Result::Empty);
} else {
return SCPI::getResultName(SCPI::Result::Error);
}
}, [=](QStringList params) -> QString {
auto t = findTrace(params);
if(!t) {
return SCPI::getResultName(SCPI::Result::Error);
}
return Trace::TypeToString(t->liveType());
}));
}
void TraceWidget::contextMenuEvent(QContextMenuEvent *event)
{
auto index = ui->view->indexAt(event->pos());
if(!index.isValid()) {
return;
}
auto trace = model.trace(index.row());
auto ctxmenu = new QMenu();
auto action_delete = new QAction("Delete");
connect(action_delete, &QAction::triggered, this, &TraceWidget::on_remove_clicked);
ctxmenu->addAction(action_delete);
auto action_duplicate = new QAction("Duplicate");
connect(action_duplicate, &QAction::triggered, this, [=](){
auto json = trace->toJSON();
auto duplicate = new Trace();
duplicate->fromJSON(json);
duplicate->setName(duplicate->name() + " - Duplicate");
// force update of hash
duplicate->toHash(true);
model.addTrace(duplicate);
// resolve math sources
if(!duplicate->resolveMathSourceHashes()) {
qWarning() << "Failed to resolve all math source hashes for"<<duplicate;
}
});
ctxmenu->addAction(action_duplicate);
ctxmenu->exec(event->globalPos());
}