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..51c7ead 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -102,8 +102,10 @@ HEADERS += \ Traces/tracewaterfall.h \ Traces/tracewidget.h \ Traces/tracexyplot.h \ + Traces/tracepolar.h \ Traces/waterfallaxisdialog.h \ Traces/xyplotaxisdialog.h \ + Traces/tracepolarchart.h \ Util/qpointervariant.h \ Util/util.h \ Util/app_common.h \ @@ -222,10 +224,12 @@ SOURCES += \ Traces/tracemodel.cpp \ Traces/traceplot.cpp \ Traces/tracesmithchart.cpp \ + Traces/tracepolarchart.cpp \ Traces/tracetouchstoneexport.cpp \ Traces/tracewaterfall.cpp \ Traces/tracewidget.cpp \ Traces/tracexyplot.cpp \ + Traces/tracepolar.cpp \ Traces/waterfallaxisdialog.cpp \ Traces/xyplotaxisdialog.cpp \ Util/util.cpp \ @@ -293,6 +297,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/smithchartdialog.ui b/Software/PC_Application/Traces/smithchartdialog.ui index 7843bad..1adddc7 100644 --- a/Software/PC_Application/Traces/smithchartdialog.ui +++ b/Software/PC_Application/Traces/smithchartdialog.ui @@ -117,6 +117,16 @@ + + + + Offset real axis + + + + + + 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/tracepolar.cpp b/Software/PC_Application/Traces/tracepolar.cpp new file mode 100644 index 0000000..942e643 --- /dev/null +++ b/Software/PC_Application/Traces/tracepolar.cpp @@ -0,0 +1,378 @@ +#include "tracepolar.h" + +#include "Marker/marker.h" +#include "Util/util.h" + +#include + +using namespace std; + +TracePolar::TracePolar(TraceModel &model, QWidget *parent) + : TracePlot(model, parent) +{ + limitToSpan = true; + limitToEdge = true; + edgeReflection = 1.0; + dx = 0; + initializeTraceInfo(); +} + +nlohmann::json TracePolar::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; +} + +void TracePolar::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; + } + } +} + +void TracePolar::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(); +} + +QPoint TracePolar::dataToPixel(std::complex d) +{ + return transform.map(QPoint(d.real() * polarCoordMax * (1.0 / edgeReflection), -d.imag() * polarCoordMax * (1.0 / edgeReflection))); +} + +QPoint TracePolar:: dataToPixel(Trace::Data d) +{ + return dataToPixel(d.y); +} + +std::complex TracePolar::dataAddDx(std::complex d) +{ + auto dataShift = complex(dx, 0); + d = d + dataShift; + return d; +} + +Trace::Data TracePolar::dataAddDx(Trace::Data d) +{ + d.y = dataAddDx(d.y); + return d; +} + +std::complex TracePolar::pixelToData(QPoint p) +{ + auto data = transform.inverted().map(QPointF(p)); + return complex(data.x() / polarCoordMax * edgeReflection, -data.y() / polarCoordMax * edgeReflection); +} + +QPoint TracePolar::markerToPixel(Marker *m) +{ + QPoint ret = QPoint(); +// if(!m->isTimeDomain()) { + if(m->getPosition() >= sweep_fmin && m->getPosition() <= sweep_fmax) { + auto d = m->getData(); + d = dataAddDx(d); + ret = dataToPixel(d); + } +// } + return ret; +} + +double TracePolar::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 TracePolar::markerVisible(double x) +{ + if(limitToSpan) { + if(x >= sweep_fmin && x <= sweep_fmax) { + return true; + } else { + return false; + } + } else { + // complete traces visible + return true; + } +} + +void TracePolar::updateContextMenu() +{ + contextmenu->clear(); + auto setup = new QAction("Setup...", contextmenu); + connect(setup, &QAction::triggered, this, &TracePolar::axisSetupDialog, Qt::UniqueConnection); + 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(); +} + +bool TracePolar::constrainLineToCircle(QPointF &a, QPointF &b, QPointF center, double radius) +{ + auto distance = [](const QPointF &a, const QPointF &b) { + auto dx = b.x() - a.x(); + auto dy = b.y() - a.y(); + return sqrt(dx*dx + dy*dy); + }; + + if(distance(a, center) <= radius && distance(b, center) <= radius) { + // both points are completely contained within the circle, no adjustment necessary + return true; + } + + // shift points, the formulas assume center = (0,0) + a -= center; + b -= center; + + // according to https://mathworld.wolfram.com/Circle-LineIntersection.html + auto dx = b.x() - a.x(); + auto dy = b.y() - a.y(); + auto dr = sqrt(dx*dx+dy*dy); + auto D = a.x()*b.y() - b.x()*a.y(); + // check intersection + auto delta = radius*radius * dr*dr - D*D; + if(delta <= 0) { + // line does not intersect the circle + return false; + } + // line intersects the circle, calculate intersection points + auto x1 = (D*dy+copysign(1.0, dy) * dx*sqrt(delta)) / (dr*dr); + auto x2 = (D*dy-copysign(1.0, dy) * dx*sqrt(delta)) / (dr*dr); + auto y1 = (-D*dx+abs(dy)*sqrt(delta)) / (dr*dr); + auto y2 = (-D*dx-abs(dy)*sqrt(delta)) / (dr*dr); + + auto inter1 = QPointF(x1, y1); + auto inter2 = QPointF(x2, y2); + + bool inter1betweenPoints = false; + bool inter2betweenPoints = false; + if(abs(distance(a, inter1) + distance(b, inter1) - distance(a, b)) < 0.000001) { + inter1betweenPoints = true; + } + if(abs(distance(a, inter2) + distance(b, inter2) - distance(a, b)) < 0.000001) { + inter2betweenPoints = true; + } + if(inter1betweenPoints && inter2betweenPoints) { + // adjust both points, order does not matter + a = inter1; + b = inter2; + } else { + // exactly one intersection point must lie between the two line points, otherwise we would have returned already + auto inter = inter1betweenPoints ? inter1 : inter2; + if(distance(a, QPointF(0,0)) < radius) { + // point is in the circle and can remain unchanged. Use inter as new point b + b = inter; + } else { + // the other way around + a = inter; + } + } + a += center; + b += center; + return true; +} + +PolarArc::PolarArc(QPointF center, double radius, double startAngle, double spanAngle) + : center(center), + radius(radius), + startAngle(startAngle), + spanAngle(spanAngle) +{ + +} + +void PolarArc::constrainToCircle(QPointF center, double radius) +{ + // check if arc/circle intersect + auto centerDiff = this->center - center; + auto centerDistSquared = centerDiff.x() * centerDiff.x() + centerDiff.y() * centerDiff.y(); + if (centerDistSquared >= (radius + this->radius) * (radius + this->radius)) { + // arc completely outside of constraining circle + spanAngle = 0.0; + return; + } else if (centerDistSquared <= (radius - this->radius) * (radius - this->radius)) { + if (radius >= this->radius) { + // arc completely in constraining circle, do nothing + return; + } else { + // arc completely outside of circle + spanAngle = 0.0; + return; + } + } else { + // there are intersections between the arc and the circle. Calculate points according to https://stackoverflow.com/questions/3349125/circle-circle-intersection-points + auto distance = sqrt(centerDistSquared); + auto a = (this->radius*this->radius-radius*radius+distance*distance) / (2*distance); + auto h = sqrt(this->radius*this->radius - a*a); + auto intersectMiddle = this->center + a*(center - this->center) / distance; + auto rotatedCenterDiff = center - this->center; + swap(rotatedCenterDiff.rx(), rotatedCenterDiff.ry()); + rotatedCenterDiff.setY(-rotatedCenterDiff.y()); + auto intersect1 = intersectMiddle + h * rotatedCenterDiff / distance; + auto intersect2 = intersectMiddle - h * rotatedCenterDiff / distance; + + // got intersection points, convert into angles from arc center + auto wrapAngle = [](double angle) -> double { + double ret = fmod(angle, 2*M_PI); + if(ret < 0) { + ret += 2*M_PI; + } + return ret; + }; + + auto angle1 = wrapAngle(atan2((intersect1 - this->center).y(), (intersect1 - this->center).x())); + auto angle2 = wrapAngle(atan2((intersect2 - this->center).y(), (intersect2 - this->center).x())); + + auto angleDiff = wrapAngle(angle2 - angle1); + if ((angleDiff >= M_PI) ^ (a > 0.0)) { + // allowed angles go from intersect1 to intersect2 + if(startAngle < angle1) { + startAngle = angle1; + } + auto maxSpan = wrapAngle(angle2 - startAngle); + if(spanAngle > maxSpan) { + spanAngle = maxSpan; + } + } else { + // allowed angles go from intersect2 to intersect1 + if(startAngle < angle2) { + startAngle = angle2; + } + auto maxSpan = wrapAngle(angle1 - startAngle); + if(spanAngle > maxSpan) { + spanAngle = maxSpan; + } + } + } +} + diff --git a/Software/PC_Application/Traces/tracepolar.h b/Software/PC_Application/Traces/tracepolar.h new file mode 100644 index 0000000..2d878a4 --- /dev/null +++ b/Software/PC_Application/Traces/tracepolar.h @@ -0,0 +1,58 @@ +#ifndef TRACEPOLAR_H +#define TRACEPOLAR_H + +#include "traceplot.h" + +class PolarArc { +public: + PolarArc(QPointF center, double radius, double startAngle = 0.0, double spanAngle = 2*M_PI); + void constrainToCircle(QPointF center, double radius); + QPointF center; + double radius; + double startAngle, spanAngle; +}; + +class TracePolar : public TracePlot +{ + Q_OBJECT +public: + TracePolar(TraceModel &model, QWidget *parent = 0); + + virtual nlohmann::json toJSON() override; // derived classes must call TracePolar::joJSON before doing anything + virtual void fromJSON(nlohmann::json j) override; // derived classes must call TracePolar::joJSON before doing anything + + void wheelEvent(QWheelEvent *event) override; + +public slots: + virtual void axisSetupDialog() {}; + +protected: + static constexpr double polarCoordMax = 4096; + + virtual std::complex dataAddDx(std::complex d); + virtual 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) override; + + virtual void updateContextMenu() override; + virtual bool supported(Trace *t) override {Q_UNUSED(t) return false;}; + + // given two points and a circle, the two points are adjusted in such a way that the line they describe + // is constrained within the circle. Returns true if there is a remaining line segment in the circle, false + // if the line lies completely outside of the circle (or is tangent to the circle) + static bool constrainLineToCircle(QPointF &a, QPointF &b, QPointF center, double radius); + + bool limitToSpan; + bool limitToEdge; + double edgeReflection; // magnitude of reflection coefficient at the edge of the polar chart (zoom factor) + double dx; + QTransform transform; +}; + +#endif // TRACEPOLAR_H diff --git a/Software/PC_Application/Traces/tracepolarchart.cpp b/Software/PC_Application/Traces/tracepolarchart.cpp new file mode 100644 index 0000000..7fd0c97 --- /dev/null +++ b/Software/PC_Application/Traces/tracepolarchart.cpp @@ -0,0 +1,258 @@ +#include "tracepolarchart.h" + +#include "ui_polarchartdialog.h" +#include "preferences.h" +#include "unit.h" +#include "appwindow.h" + +#include +#include + +using namespace std; + +TracePolarChart::TracePolarChart(TraceModel &model, QWidget *parent) + : TracePolar(model, parent) +{ +} + +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(); + } +} + +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 = [&](PolarArc 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(PolarArc(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); + + // scale to size of smith diagram + QPointF p1 = dataToPixel(last); + QPointF p2 = dataToPixel(now); + + if(limitToEdge && (abs(last.y) > edgeReflection || abs(now.y) > edgeReflection)) { + // partially outside of visible area, constrain + if(!TracePolar::constrainLineToCircle(p1, p2, transform.map(QPointF(0,0)), polarCoordMax * scale)) { + // completely out of visible area + continue; + } + } + + // 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 { + } + +} + +bool TracePolarChart::dropSupported(Trace *t) +{ + switch(t->outputType()) { + case Trace::DataType::Frequency: + return true; + default: + return false; + } +} + +bool TracePolarChart::supported(Trace *t) +{ + return dropSupported(t); +} + +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(); + } +} diff --git a/Software/PC_Application/Traces/tracepolarchart.h b/Software/PC_Application/Traces/tracepolarchart.h new file mode 100644 index 0000000..18b89f4 --- /dev/null +++ b/Software/PC_Application/Traces/tracepolarchart.h @@ -0,0 +1,24 @@ +#ifndef TRACEPOLARCHART_H +#define TRACEPOLARCHART_H + +#include "tracepolar.h" + +class TracePolarChart : public TracePolar +{ + Q_OBJECT +public: + TracePolarChart(TraceModel &model, QWidget *parent = 0); + + virtual Type getType() override { return Type::PolarChart;}; + +public slots: + virtual void axisSetupDialog() override; + +private: + bool supported(Trace *t) override; + virtual void draw(QPainter& painter) override; + virtual bool dropSupported(Trace *t) override; + QString mouseText(QPoint pos) override; +}; + +#endif // TRACEPOLARCHART_H diff --git a/Software/PC_Application/Traces/tracesmithchart.cpp b/Software/PC_Application/Traces/tracesmithchart.cpp index e690320..735d397 100644 --- a/Software/PC_Application/Traces/tracesmithchart.cpp +++ b/Software/PC_Application/Traces/tracesmithchart.cpp @@ -1,11 +1,9 @@ #include "tracesmithchart.h" -#include "Marker/marker.h" #include "preferences.h" #include "ui_smithchartdialog.h" #include "unit.h" #include "QFileDialog" -#include "Util/util.h" #include "appwindow.h" #include "CustomWidgets/informationbox.h" @@ -18,29 +16,16 @@ using namespace std; TraceSmithChart::TraceSmithChart(TraceModel &model, QWidget *parent) - : TracePlot(model, parent) + : TracePolar(model, parent) { - limitToSpan = true; - limitToEdge = true; - edgeReflection = 1.0; Z0 = 50.0; - initializeTraceInfo(); } nlohmann::json TraceSmithChart::toJSON() { nlohmann::json j; - j["limit_to_span"] = limitToSpan; - j["limit_to_edge"] = limitToEdge; - j["edge_reflection"] = edgeReflection; + j = TracePolar::toJSON(); j["Z0"] = Z0; - nlohmann::json jtraces; - for(auto t : traces) { - if(t.second) { - jtraces.push_back(t.first->toHash()); - } - } - j["traces"] = jtraces; nlohmann::json jlines; for(auto line : constantLines) { jlines.push_back(line.toJSON()); @@ -51,24 +36,8 @@ nlohmann::json TraceSmithChart::toJSON() void TraceSmithChart::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); + TracePolar::fromJSON(j); Z0 = j.value("Z0", 50.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; - } - } if(j.contains("constantLines")) { for(auto jline : j["constantLines"]) { SmithChartConstantLine line; @@ -78,19 +47,6 @@ void TraceSmithChart::fromJSON(nlohmann::json j) } } -void TraceSmithChart::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; - triggerReplot(); -} - void TraceSmithChart::axisSetupDialog() { auto dialog = new QDialog(); @@ -110,6 +66,7 @@ void TraceSmithChart::axisSetupDialog() ui->zoomFactor->setPrecision(3); ui->zoomReflection->setValue(edgeReflection); ui->zoomFactor->setValue(1.0/edgeReflection); + ui->offsetRealAxis->setValue(dx); ui->impedance->setUnit("Ω"); ui->impedance->setPrecision(3); @@ -133,6 +90,9 @@ void TraceSmithChart::axisSetupDialog() edgeReflection = ui->zoomReflection->value(); ui->zoomFactor->setValueQuiet(1.0 / edgeReflection); }); + connect(ui->offsetRealAxis, &SIUnitEdit::valueChanged, [=](){ + dx = ui->offsetRealAxis->value(); + }); connect(ui->impedance, &SIUnitEdit::valueChanged, [=](){ Z0 = ui->impedance->value(); for(auto t : traces) { @@ -189,95 +149,6 @@ void TraceSmithChart::axisSetupDialog() } } -QPoint TraceSmithChart::dataToPixel(std::complex d) -{ - return transform.map(QPoint(d.real() * smithCoordMax * (1.0 / edgeReflection), -d.imag() * smithCoordMax * (1.0 / edgeReflection))); -} - -QPoint TraceSmithChart::dataToPixel(Trace::Data d) -{ - return dataToPixel(d.y);} - -std::complex TraceSmithChart::pixelToData(QPoint p) -{ - auto data = transform.inverted().map(QPointF(p)); - return complex(data.x() / smithCoordMax * edgeReflection, -data.y() / smithCoordMax * edgeReflection); -} - -QPoint TraceSmithChart::markerToPixel(Marker *m) -{ - QPoint ret = QPoint(); -// if(!m->isTimeDomain()) { - if(m->getPosition() >= sweep_fmin && m->getPosition() <= sweep_fmax) { - auto d = m->getData(); - ret = dataToPixel(d); - } -// } - return ret; -} - -double TraceSmithChart::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); - 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(t->sample(closestIndex-1)); - auto l2 = dataToPixel(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(t->sample(closestIndex)); - auto l2 = dataToPixel(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 TraceSmithChart::markerVisible(double x) -{ - if(limitToSpan) { - if(x >= sweep_fmin && x <= sweep_fmax) { - return true; - } else { - return false; - } - } else { - // complete traces visible - return true; - } -} - bool TraceSmithChart::configureForTrace(Trace *t) { if(dropSupported(t)) { @@ -299,7 +170,7 @@ void TraceSmithChart::draw(QPainter &p) { auto w = p.window(); p.save(); p.translate(w.width()/2, w.height()/2); - auto scale = qMin(w.height(), w.width()) / (2.0 * smithCoordMax); + auto scale = qMin(w.height(), w.width()) / (2.0 * polarCoordMax); p.scale(scale, scale); transform = p.transform(); @@ -326,8 +197,8 @@ void TraceSmithChart::draw(QPainter &p) { p.setPen(pen); for(int i=1;i(edgeReflection,0)),dataToPixel(complex(-edgeReflection,0))); @@ -335,8 +206,8 @@ void TraceSmithChart::draw(QPainter &p) { for(auto z : impedanceLines) { z /= Z0; auto radius = 1.0/z; - drawArc(SmithChartArc(QPointF(1.0, radius), radius, 0, 2*M_PI)); - drawArc(SmithChartArc(QPointF(1.0, -radius), radius, 0, 2*M_PI)); + drawArc(SmithChartArc(QPointF(1.0+dx, radius), radius, 0, 2*M_PI)); + drawArc(SmithChartArc(QPointF(1.0+dx, -radius), radius, 0, 2*M_PI)); } // draw custom constant parameter lines @@ -372,13 +243,22 @@ void TraceSmithChart::draw(QPainter &p) { if(isnan(now.y.real())) { break; } - if (limitToEdge && (abs(last.y) > edgeReflection || abs(now.y) > edgeReflection)) { - // outside of visible area - continue; - } + + last = dataAddDx(last); + now = dataAddDx(now); + // scale to size of smith diagram - auto p1 = dataToPixel(last); - auto p2 = dataToPixel(now); + QPointF p1 = dataToPixel(last); + QPointF p2 = dataToPixel(now); + + if(limitToEdge && (abs(last.y) > edgeReflection || abs(now.y) > edgeReflection)) { + // partially outside of visible area, constrain + if(!TracePolar::constrainLineToCircle(p1, p2, transform.map(QPointF(0,0)), polarCoordMax * scale)) { + // completely out of visible area + continue; + } + } + // draw line p.drawLine(p1, p2); } @@ -400,6 +280,8 @@ void TraceSmithChart::draw(QPainter &p) { continue; } auto coords = m->getData(); + coords = dataAddDx(coords); + if (limitToEdge && abs(coords) > edgeReflection) { // outside of visible area continue; @@ -415,7 +297,7 @@ void TraceSmithChart::draw(QPainter &p) { p.setOpacity(0.5); p.setBrush(Qt::white); p.setPen(Qt::white); - p.drawEllipse(-smithCoordMax, -smithCoordMax, 2*smithCoordMax, 2*smithCoordMax); + p.drawEllipse(-polarCoordMax, -polarCoordMax, 2*polarCoordMax, 2*polarCoordMax); auto font = p.font(); font.setPixelSize(20); p.setFont(font); @@ -459,8 +341,9 @@ bool TraceSmithChart::dropSupported(Trace *t) QString TraceSmithChart::mouseText(QPoint pos) { - auto data = pixelToData(pos); - if(abs(data) <= edgeReflection) { + auto dataDx = pixelToData(pos); + if(abs(dataDx) <= edgeReflection) { + auto data = complex(dataDx.real()-dx, dataDx.imag()); data = Z0 * (1.0 + data) / (1.0 - data); auto ret = Unit::ToString(data.real(), "", " ", 3); if(data.imag() >= 0) { @@ -473,64 +356,6 @@ QString TraceSmithChart::mouseText(QPoint pos) } } -void TraceSmithChart::updateContextMenu() -{ - contextmenu->clear(); - auto setup = new QAction("Setup...", contextmenu); - connect(setup, &QAction::triggered, this, &TraceSmithChart::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(); -} - bool TraceSmithChart::supported(Trace *t) { if(t->getReferenceImpedance() != Z0) { diff --git a/Software/PC_Application/Traces/tracesmithchart.h b/Software/PC_Application/Traces/tracesmithchart.h index c8f9276..0764124 100644 --- a/Software/PC_Application/Traces/tracesmithchart.h +++ b/Software/PC_Application/Traces/tracesmithchart.h @@ -1,7 +1,7 @@ #ifndef TRACESMITHCHART_H #define TRACESMITHCHART_H -#include "traceplot.h" +#include "tracepolar.h" #include #include @@ -98,7 +98,7 @@ private: TraceSmithChart &chart; }; -class TraceSmithChart : public TracePlot +class TraceSmithChart : public TracePolar { Q_OBJECT friend class SmithChartContantLineModel; @@ -110,34 +110,17 @@ public: virtual nlohmann::json toJSON() override; virtual void fromJSON(nlohmann::json j) override; - void wheelEvent(QWheelEvent *event) override; public slots: - void axisSetupDialog(); + virtual void axisSetupDialog() override; protected: - static constexpr double screenUsage = 0.9; - static constexpr double smithCoordMax = 4096; - - 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); - - //void paintEvent(QPaintEvent *event) override; - virtual bool configureForTrace(Trace *t); - virtual void updateContextMenu() override; + virtual bool configureForTrace(Trace *t) override; bool supported(Trace *t) override; virtual void draw(QPainter& painter) override; virtual void traceDropped(Trace *t, QPoint position) override; virtual bool dropSupported(Trace *t) override; QString mouseText(QPoint pos) override; - bool limitToSpan; - bool limitToEdge; - double edgeReflection; // magnitude of reflection coefficient at the edge of the smith chart (zoom factor) double Z0; - QTransform transform; std::vector constantLines; };