From 5b26a4a9c1ccda0cde4fb704e66d3e82858ae5d3 Mon Sep 17 00:00:00 2001 From: Kiara Navarro Date: Sun, 17 Jul 2022 09:32:55 -0500 Subject: [PATCH] widgets/charts: implement polar diagram --- .../CustomWidgets/tilewidget.cpp | 11 + .../PC_Application/CustomWidgets/tilewidget.h | 1 + .../CustomWidgets/tilewidget.ui | 7 + Software/PC_Application/LibreVNA-GUI.pro | 3 + .../PC_Application/Traces/polarchartdialog.ui | 172 ++++++ Software/PC_Application/Traces/traceplot.h | 1 + .../PC_Application/Traces/tracepolarchart.cpp | 491 ++++++++++++++++++ .../PC_Application/Traces/tracepolarchart.h | 56 ++ 8 files changed, 742 insertions(+) create mode 100644 Software/PC_Application/Traces/polarchartdialog.ui create mode 100644 Software/PC_Application/Traces/tracepolarchart.cpp create mode 100644 Software/PC_Application/Traces/tracepolarchart.h diff --git a/Software/PC_Application/CustomWidgets/tilewidget.cpp b/Software/PC_Application/CustomWidgets/tilewidget.cpp index 2c8e1cb..980b37e 100644 --- a/Software/PC_Application/CustomWidgets/tilewidget.cpp +++ b/Software/PC_Application/CustomWidgets/tilewidget.cpp @@ -4,6 +4,7 @@ #include "Traces/tracexyplot.h" #include "Traces/tracesmithchart.h" #include "Traces/tracewaterfall.h" +#include "Traces/tracepolarchart.h" #include @@ -72,6 +73,9 @@ nlohmann::json TileWidget::toJSON() case TracePlot::Type::Waterfall: plotname = "Waterfall"; break; + case TracePlot::Type::PolarChart: + plotname = "PolarChart"; + break; } j["plot"] = plotname; j["plotsettings"] = content->toJSON(); @@ -102,6 +106,8 @@ void TileWidget::fromJSON(nlohmann::json j) content = new TraceXYPlot(model); } else if (plotname == "Waterfall"){ content = new TraceWaterfall(model); + } else if (plotname == "PolarChart"){ + content = new TracePolarChart(model); } if(content) { setContent(content); @@ -311,3 +317,8 @@ void TileWidget::on_bWaterfall_clicked() setContent(new TraceWaterfall(model)); } +void TileWidget::on_bPolarchart_clicked() +{ + setContent(new TracePolarChart(model)); +} + diff --git a/Software/PC_Application/CustomWidgets/tilewidget.h b/Software/PC_Application/CustomWidgets/tilewidget.h index 66e385b..30a58c6 100644 --- a/Software/PC_Application/CustomWidgets/tilewidget.h +++ b/Software/PC_Application/CustomWidgets/tilewidget.h @@ -45,6 +45,7 @@ private slots: void plotDeleted(); void on_bWaterfall_clicked(); + void on_bPolarchart_clicked(); private: TileWidget(TraceModel &model, TileWidget &parent); diff --git a/Software/PC_Application/CustomWidgets/tilewidget.ui b/Software/PC_Application/CustomWidgets/tilewidget.ui index 57d3292..0724cb8 100644 --- a/Software/PC_Application/CustomWidgets/tilewidget.ui +++ b/Software/PC_Application/CustomWidgets/tilewidget.ui @@ -92,6 +92,13 @@ + + + + Polar Chart + + + diff --git a/Software/PC_Application/LibreVNA-GUI.pro b/Software/PC_Application/LibreVNA-GUI.pro index b12ef96..a603252 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -104,6 +104,7 @@ HEADERS += \ Traces/tracexyplot.h \ Traces/waterfallaxisdialog.h \ Traces/xyplotaxisdialog.h \ + Traces/tracepolarchart.h \ Util/qpointervariant.h \ Util/util.h \ Util/app_common.h \ @@ -222,6 +223,7 @@ SOURCES += \ Traces/tracemodel.cpp \ Traces/traceplot.cpp \ Traces/tracesmithchart.cpp \ + Traces/tracepolarchart.cpp \ Traces/tracetouchstoneexport.cpp \ Traces/tracewaterfall.cpp \ Traces/tracewidget.cpp \ @@ -293,6 +295,7 @@ FORMS += \ Traces/Math/timegateexplanationwidget.ui \ Traces/XYPlotConstantLineEditDialog.ui \ Traces/smithchartdialog.ui \ + Traces/polarchartdialog.ui \ Traces/tracecsvexport.ui \ Traces/traceeditdialog.ui \ Traces/traceimportdialog.ui \ diff --git a/Software/PC_Application/Traces/polarchartdialog.ui b/Software/PC_Application/Traces/polarchartdialog.ui new file mode 100644 index 0000000..56fc831 --- /dev/null +++ b/Software/PC_Application/Traces/polarchartdialog.ui @@ -0,0 +1,172 @@ + + + PolarChartDialog + + + + 0 + 0 + 389 + 275 + + + + Polart Chart Setup + + + true + + + + + + + + + + Display mode + + + + + + Frequency: + + + + + + + + Show complete traces + + + + + Limit to current span + + + + + + + + Γ + + + + + + + + Show complete traces + + + + + Limit to visible area + + + + + + + + + + + Zoom + + + + + + Factor: + + + + + + + + + + <html><head/><body><p>|Γ| at edge:</p></body></html> + + + + + + + + + + Offset real axis: + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + SIUnitEdit + QLineEdit +
CustomWidgets/siunitedit.h
+
+
+ + + + buttonBox + accepted() + PolarChartDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PolarChartDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/Software/PC_Application/Traces/traceplot.h b/Software/PC_Application/Traces/traceplot.h index da4a3f5..5918452 100644 --- a/Software/PC_Application/Traces/traceplot.h +++ b/Software/PC_Application/Traces/traceplot.h @@ -20,6 +20,7 @@ public: SmithChart, XYPlot, Waterfall, + PolarChart, }; TracePlot(TraceModel &model, QWidget *parent = nullptr); diff --git a/Software/PC_Application/Traces/tracepolarchart.cpp b/Software/PC_Application/Traces/tracepolarchart.cpp new file mode 100644 index 0000000..dee64dc --- /dev/null +++ b/Software/PC_Application/Traces/tracepolarchart.cpp @@ -0,0 +1,491 @@ +#include "tracepolarchart.h" + +#include "ui_polarchartdialog.h" +#include "preferences.h" +#include "tracesmithchart.h" +#include "unit.h" +#include "Marker/marker.h" +#include "Util/util.h" +#include "appwindow.h" + +#include +#include + +using namespace std; + +TracePolarChart::TracePolarChart(TraceModel &model, QWidget *parent) + : TracePlot(model, parent) +{ + limitToSpan = true; + limitToEdge = true; + edgeReflection = 1.0; + dx = 0.0; + initializeTraceInfo(); +} + +void TracePolarChart::wheelEvent(QWheelEvent *event) +{ + // most mousewheel have 15 degree increments, the reported delta is in 1/8th degree -> 120 + auto increment = event->angleDelta().y() / 120.0; + // round toward bigger step in case of special higher resolution mousewheel + int steps = increment > 0 ? ceil(increment) : floor(increment); + + constexpr double zoomfactor = 1.1; + auto zoom = pow(zoomfactor, steps); + edgeReflection /= zoom; + + auto incrementX = event->angleDelta().x() / 120.0; + dx += incrementX/10; + triggerReplot(); +} + +void TracePolarChart::axisSetupDialog() +{ + auto dialog = new QDialog(); + auto ui = new Ui::PolarChartDialog(); + ui->setupUi(dialog); + if(limitToSpan) { + ui->displayModeFreq->setCurrentIndex(1); + } else { + ui->displayModeFreq->setCurrentIndex(0); + } + if(limitToEdge) { + ui->displayModeRefl->setCurrentIndex(1); + } else { + ui->displayModeRefl->setCurrentIndex(0); + } + ui->zoomReflection->setPrecision(3); + ui->zoomFactor->setPrecision(3); + ui->offsetRealAxis->setPrecision(3); + ui->zoomReflection->setValue(edgeReflection); + ui->zoomFactor->setValue(1.0/edgeReflection); + ui->offsetRealAxis->setValue(dx); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){ + limitToSpan = ui->displayModeFreq->currentIndex() == 1; + limitToEdge = ui->displayModeRefl->currentIndex() == 1; + triggerReplot(); + }); + connect(ui->zoomFactor, &SIUnitEdit::valueChanged, [=](){ + edgeReflection = 1.0 / ui->zoomFactor->value(); + ui->zoomReflection->setValueQuiet(edgeReflection); + }); + connect(ui->zoomReflection, &SIUnitEdit::valueChanged, [=](){ + edgeReflection = ui->zoomReflection->value(); + ui->zoomFactor->setValueQuiet(1.0 / edgeReflection); + }); + connect(ui->offsetRealAxis, &SIUnitEdit::valueChanged, [=](){ + dx = ui->offsetRealAxis->value(); + }); + if(AppWindow::showGUI()) { + dialog->show(); + } +} + +QPoint TracePolarChart::dataToPixel(std::complex d) +{ + return transform.map(QPoint(d.real() * polarCoordMax * (1.0 / edgeReflection), -d.imag() * polarCoordMax * (1.0 / edgeReflection))); +} + +QPoint TracePolarChart::dataToPixel(Trace::Data d) +{ + return dataToPixel(d.y); +} + +std::complex TracePolarChart::dataAddDx(std::complex d) +{ + auto dataShift = complex(dx, 0); + d = d + dataShift; + return d; +} + +Trace::Data TracePolarChart::dataAddDx(Trace::Data d) +{ + d.y = dataAddDx(d.y); + return d; +} + +std::complex TracePolarChart::pixelToData(QPoint p) +{ + auto data = transform.inverted().map(QPointF(p)); + return complex(data.x() / polarCoordMax * edgeReflection, -data.y() / polarCoordMax * edgeReflection); +} + +void TracePolarChart::draw(QPainter &p) { + auto pref = Preferences::getInstance(); + + p.setRenderHint(QPainter::Antialiasing); + auto w = p.window(); + p.save(); + p.translate(w.width()/2, w.height()/2); + auto scale = qMin(w.height(), w.width()) / (2.0 * polarCoordMax); + p.scale(scale, scale); + + transform = p.transform(); + p.restore(); + + auto drawArc = [&](SmithChartArc a) { + a.constrainToCircle(QPointF(0,0), edgeReflection); + auto topleft = dataToPixel(complex(a.center.x() - a.radius, a.center.y() - a.radius)); + auto bottomright = dataToPixel(complex(a.center.x() + a.radius, a.center.y() + a.radius)); + a.startAngle *= 5760 / (2*M_PI); + a.spanAngle *= 5760 / (2*M_PI); + p.drawArc(QRect(topleft, bottomright), a.startAngle, a.spanAngle); + }; + + // Outer circle + auto pen = QPen(pref.Graphs.Color.axis); + pen.setCosmetic(true); + p.setPen(pen); + drawArc(SmithChartArc(QPointF(0.0, 0.0), edgeReflection, 0, 2*M_PI)); + + constexpr int Circles = 6; + pen = QPen(pref.Graphs.Color.Ticks.divisions, 0.5, Qt::DashLine); + pen.setCosmetic(true); + p.setPen(pen); + for(int i=1;i(dx, cir.center.y() + cir.radius*sin(angle)); + auto p2 = complex(dx, cir.center.y() - cir.radius*sin(angle)); + p.drawLine(dataToPixel(p1),dataToPixel(p2)); + } + else { + auto slope = tan(cir.spanAngle*2*M_PI/360); + auto y0 = cir.center.y(); + auto f = dx; + auto a = 1 + (slope*slope); + auto b = (-2*cir.center.x())-(2*f*slope*slope)+(2*slope*y0)-(2*cir.center.y()*slope); + auto c = (cir.center.x()*cir.center.x()) +(cir.center.y()*cir.center.y()) - (cir.radius*cir.radius) + (y0*y0) \ + + (slope*slope*f*f) - (2 * slope * f * y0 ) \ + + (2*cir.center.y()*slope*f)-(2*cir.center.y()*y0); + auto D = (b*b) - (4 * a * c); + + auto x1 = (-b + sqrt(D))/(2*a); + auto x2 = (-b - sqrt(D))/(2*a); + auto y1 = slope*(x1-f)+y0; + auto y2 = slope*(x2-f)+y0; + + auto p1 = complex(x1,y1); + auto p2 = complex(x2,y2); + p.drawLine(dataToPixel(p1),dataToPixel(p2)); + } + }; + + constexpr int Lines = 6; + for(int i=0;iisVisible()) { + // trace marked invisible + continue; + } + pen = QPen(trace->color(), pref.Graphs.lineWidth); + pen.setCosmetic(true); + p.setPen(pen); + int nPoints = trace->size(); + for(int i=1;isample(i-1); + auto now = trace->sample(i); + if (limitToSpan && (trace->getDataType() == Trace::DataType::Frequency) && (last.x < sweep_fmin || now.x > sweep_fmax)) { + continue; + } + if(isnan(now.y.real())) { + break; + } + + last = dataAddDx(last); + now = dataAddDx(now); + + if (limitToEdge && (abs(last.y) > edgeReflection || abs(now.y) > edgeReflection)) { + // outside of visible area + continue; + } + // scale to size of smith diagram + auto p1 = dataToPixel(last); + auto p2 = dataToPixel(now); + // draw line + p.drawLine(p1, p2); + } + if(trace->size() > 0) { + // only draw markers if the trace has at least one point + auto markers = t.first->getMarkers(); + for(auto m : markers) { + if(!m->isVisible()) { + continue; + } + if (limitToSpan && (m->getPosition() < sweep_fmin || m->getPosition() > sweep_fmax)) { + continue; + } + if(m->getPosition() < trace->minX() || m->getPosition() > trace->maxX()) { + // marker not in trace range + continue; + } + auto coords = m->getData(); + coords = dataAddDx(coords); + + if (limitToEdge && abs(coords) > edgeReflection) { + // outside of visible area + continue; + } + auto point = dataToPixel(coords); + auto symbol = m->getSymbol(); + p.drawPixmap(point.x() - symbol.width()/2, point.y() - symbol.height(), symbol); + } + } + } + + if(dropPending) { + // TODO adjust coords due to shifted restore + p.setOpacity(0.5); + p.setBrush(Qt::white); + p.setPen(Qt::white); + p.drawEllipse(-polarCoordMax, -polarCoordMax, 2*polarCoordMax, 2*polarCoordMax); + auto font = p.font(); + font.setPixelSize(20); + p.setFont(font); + p.setOpacity(1.0); + p.setPen(Qt::white); + auto text = "Drop here to add\n" + dropTrace->name() + "\nto polar chart"; + p.drawText(p.window(), Qt::AlignCenter, text); + } else { + } + +} + +void TracePolarChart::fromJSON(nlohmann::json j) +{ + limitToSpan = j.value("limit_to_span", true); + limitToEdge = j.value("limit_to_edge", false); + edgeReflection = j.value("edge_reflection", 1.0); + dx = j.value("offset_axis_x", 0.0); + for(unsigned int hash : j["traces"]) { + // attempt to find the traces with this hash + bool found = false; + for(auto t : model.getTraces()) { + if(t->toHash() == hash) { + enableTrace(t, true); + found = true; + break; + } + } + if(!found) { + qWarning() << "Unable to find trace with hash" << hash; + } + } +} + +nlohmann::json TracePolarChart::toJSON() +{ + nlohmann::json j; + j["limit_to_span"] = limitToSpan; + j["limit_to_edge"] = limitToEdge; + j["edge_reflection"] = edgeReflection; + j["offset_axis_x"] = dx; + nlohmann::json jtraces; + for(auto t : traces) { + if(t.second) { + jtraces.push_back(t.first->toHash()); + } + } + j["traces"] = jtraces; + return j; +} + +double TracePolarChart::nearestTracePoint(Trace *t, QPoint pixel, double *distance) +{ + double closestDistance = numeric_limits::max(); + double closestXpos = 0; + unsigned int closestIndex = 0; + auto samples = t->size(); + for(unsigned int i=0;isample(i); + data = dataAddDx(data); + auto plotPoint = dataToPixel(data); + if (plotPoint.isNull()) { + // destination point outside of currently displayed range + continue; + } + auto diff = plotPoint - pixel; + unsigned int distance = diff.x() * diff.x() + diff.y() * diff.y(); + if(distance < closestDistance) { + closestDistance = distance; + closestXpos = t->sample(i).x; + closestIndex = i; + } + } + closestDistance = sqrt(closestDistance); + + if(closestIndex > 0) { + auto l1 = dataToPixel(dataAddDx(t->sample(closestIndex-1))); + auto l2 = dataToPixel(dataAddDx(t->sample(closestIndex))); + double ratio; + auto distance = Util::distanceToLine(pixel, l1, l2, nullptr, &ratio); + if(distance < closestDistance) { + closestDistance = distance; + closestXpos = t->sample(closestIndex-1).x + (t->sample(closestIndex).x - t->sample(closestIndex-1).x) * ratio; + } + } + if(closestIndex < t->size() - 1) { + auto l1 = dataToPixel(dataAddDx(t->sample(closestIndex))); + auto l2 = dataToPixel(dataAddDx(t->sample(closestIndex+1))); + double ratio; + auto distance = Util::distanceToLine(pixel, l1, l2, nullptr, &ratio); + if(distance < closestDistance) { + closestDistance = distance; + closestXpos = t->sample(closestIndex).x + (t->sample(closestIndex+1).x - t->sample(closestIndex).x) * ratio; + } + } + if(distance) { + *distance = closestDistance; + } + return closestXpos; +} + +bool TracePolarChart::dropSupported(Trace *t) +{ + if(!t->isReflection()) { + return false; + } + switch(t->outputType()) { + case Trace::DataType::Frequency: + return true; + default: + return false; + } +} + +bool TracePolarChart::markerVisible(double x) +{ + if(limitToSpan) { + if(x >= sweep_fmin && x <= sweep_fmax) { + return true; + } else { + return false; + } + } else { + // complete traces visible + return true; + } +} + +bool TracePolarChart::supported(Trace *t) +{ + return dropSupported(t); +} + +void TracePolarChart::updateContextMenu() +{ + contextmenu->clear(); + auto setup = new QAction("Setup...", contextmenu); + connect(setup, &QAction::triggered, this, &TracePolarChart::axisSetupDialog); + contextmenu->addAction(setup); + + contextmenu->addSeparator(); + auto image = new QAction("Save image...", contextmenu); + contextmenu->addAction(image); + connect(image, &QAction::triggered, [=]() { + auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", "", "PNG image files (*.png)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.isEmpty()) { + // aborted selection + return; + } + if(filename.endsWith(".png")) { + filename.chop(4); + } + filename += ".png"; + grab().save(filename); + }); + + auto createMarker = contextmenu->addAction("Add marker here"); + bool activeTraces = false; + for(auto t : traces) { + if(t.second) { + activeTraces = true; + break; + } + } + if(!activeTraces) { + createMarker->setEnabled(false); + } + + connect(createMarker, &QAction::triggered, [=](){ + createMarkerAtPosition(contextmenuClickpoint); + }); + + contextmenu->addSection("Traces"); + // Populate context menu + for(auto t : orderedTraces()) { + if(!supported(t)) { + continue; + } + auto action = new QAction(t->name(), contextmenu); + action->setCheckable(true); + if(traces[t]) { + action->setChecked(true); + } + connect(action, &QAction::toggled, [=](bool active) { + enableTrace(t, active); + }); + contextmenu->addAction(action); + } + + finishContextMenu(); +} + +QPoint TracePolarChart::markerToPixel(Marker *m) +{ + QPoint ret = QPoint(); + if(m->getPosition() >= sweep_fmin && m->getPosition() <= sweep_fmax) { + auto d = m->getData(); + d = dataAddDx(d); + ret = dataToPixel(d); + } + return ret; +} + +QString TracePolarChart::mouseText(QPoint pos) +{ + auto dataDx = pixelToData(pos); + if(abs(dataDx) <= edgeReflection) { + auto data = complex(dataDx.real()-dx, dataDx.imag()); + auto ret = Unit::ToString(abs(data), "", " ", 3); + ret += QString("∠"); + auto phase = atan(data.imag()/data.real())*180/M_PI; + if (data.imag() > 0 && data.real() < 0) { + phase += 180; + } + else if (data.imag() < 0 && data.real() < 0 ) { + phase += 180; + } + else if (data.imag() < 0 && data.real() > 0) { + phase += 360; + } + ret += Unit::ToString(phase, "", " ", 3); + return ret; + } else { + return QString(); + } +} + +PolarChartCircle::PolarChartCircle(QPointF center, double radius, double startAngle, double spanAngle) + : center(center), + radius(radius), + startAngle(startAngle), + spanAngle(spanAngle) +{ + +} diff --git a/Software/PC_Application/Traces/tracepolarchart.h b/Software/PC_Application/Traces/tracepolarchart.h new file mode 100644 index 0000000..44a6a16 --- /dev/null +++ b/Software/PC_Application/Traces/tracepolarchart.h @@ -0,0 +1,56 @@ +#ifndef TRACEPOLARCHART_H +#define TRACEPOLARCHART_H + +#include "traceplot.h" + +class PolarChartCircle +{ +public: + PolarChartCircle(QPointF center, double radius, double startAngle = 0.0, double spanAngle = 2*M_PI); + QPointF center; + double radius; + double startAngle, spanAngle; +}; + + +class TracePolarChart : public TracePlot +{ + Q_OBJECT +public: + TracePolarChart(TraceModel &model, QWidget *parent = 0); + + virtual Type getType() override { return Type::PolarChart;}; + + virtual nlohmann::json toJSON() override; + virtual void fromJSON(nlohmann::json j) override; + + void wheelEvent(QWheelEvent *event) override; +public slots: + void axisSetupDialog(); + +private: + static constexpr double polarCoordMax = 4096; + + std::complex dataAddDx(std::complex d); + Trace::Data dataAddDx(Trace::Data d); + + QPoint dataToPixel(std::complex d); + QPoint dataToPixel(Trace::Data d); + std::complex pixelToData(QPoint p); + QPoint markerToPixel(Marker *m) override; + double nearestTracePoint(Trace *t, QPoint pixel, double *distance = nullptr) override; + virtual bool markerVisible(double x); + + virtual void updateContextMenu() override; + bool supported(Trace *t) override; + virtual void draw(QPainter& painter) override; + virtual bool dropSupported(Trace *t) override; + QString mouseText(QPoint pos) override; + bool limitToSpan; + bool limitToEdge; + double edgeReflection; + double dx; + QTransform transform; +}; + +#endif // TRACEPOLARCHART_H