Merge branch 'polar'

This commit is contained in:
Jan Käberich 2022-08-21 13:02:40 +02:00
commit 77bdd7fd4b
13 changed files with 962 additions and 229 deletions

View File

@ -4,6 +4,7 @@
#include "Traces/tracexyplot.h"
#include "Traces/tracesmithchart.h"
#include "Traces/tracewaterfall.h"
#include "Traces/tracepolarchart.h"
#include <QDebug>
@ -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));
}

View File

@ -45,6 +45,7 @@ private slots:
void plotDeleted();
void on_bWaterfall_clicked();
void on_bPolarchart_clicked();
private:
TileWidget(TraceModel &model, TileWidget &parent);

View File

@ -92,6 +92,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bPolarchart">
<property name="text">
<string>Polar Chart</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -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 \

View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PolarChartDialog</class>
<widget class="QDialog" name="PolarChartDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>389</width>
<height>275</height>
</rect>
</property>
<property name="windowTitle">
<string>Polart Chart Setup</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Display mode</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Frequency:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="displayModeFreq">
<item>
<property name="text">
<string>Show complete traces</string>
</property>
</item>
<item>
<property name="text">
<string>Limit to current span</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Γ</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="displayModeRefl">
<item>
<property name="text">
<string>Show complete traces</string>
</property>
</item>
<item>
<property name="text">
<string>Limit to visible area</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Zoom</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Factor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="zoomFactor"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;|Γ| at edge:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="zoomReflection"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Offset real axis:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="offsetRealAxis"/>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PolarChartDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PolarChartDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -117,6 +117,16 @@
<item row="1" column="1">
<widget class="SIUnitEdit" name="zoomReflection"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Offset real axis</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="offsetRealAxis"/>
</item>
</layout>
</widget>
</item>

View File

@ -20,6 +20,7 @@ public:
SmithChart,
XYPlot,
Waterfall,
PolarChart,
};
TracePlot(TraceModel &model, QWidget *parent = nullptr);

View File

@ -0,0 +1,378 @@
#include "tracepolar.h"
#include "Marker/marker.h"
#include "Util/util.h"
#include <QFileDialog>
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<double> 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<double> TracePolar::dataAddDx(std::complex<double> d)
{
auto dataShift = complex<double>(dx, 0);
d = d + dataShift;
return d;
}
Trace::Data TracePolar::dataAddDx(Trace::Data d)
{
d.y = dataAddDx(d.y);
return d;
}
std::complex<double> TracePolar::pixelToData(QPoint p)
{
auto data = transform.inverted().map(QPointF(p));
return complex<double>(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<double>::max();
double closestXpos = 0;
unsigned int closestIndex = 0;
auto samples = t->size();
for(unsigned int i=0;i<samples;i++) {
auto data = t->sample(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;
}
}
}
}

View File

@ -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<double> dataAddDx(std::complex<double> d);
virtual Trace::Data dataAddDx(Trace::Data d);
QPoint dataToPixel(std::complex<double> d);
QPoint dataToPixel(Trace::Data d);
std::complex<double> 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

View File

@ -0,0 +1,258 @@
#include "tracepolarchart.h"
#include "ui_polarchartdialog.h"
#include "preferences.h"
#include "unit.h"
#include "appwindow.h"
#include <QFileDialog>
#include <QPainter>
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<double>(a.center.x() - a.radius, a.center.y() - a.radius));
auto bottomright = dataToPixel(complex<double>(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<Circles;i++) {
auto radius = (double) i / Circles;
drawArc(PolarArc(QPointF(0.0 + dx,0), radius, 0, 2*M_PI));
}
auto constraintLineToCircle = [&](PolarArc cir) { // PolarArc
if ( (cir.spanAngle == 90 )&& (dx != 0.0)) {
auto angle = acos(dx/cir.radius);
auto p1 = complex<double>(dx, cir.center.y() + cir.radius*sin(angle));
auto p2 = complex<double>(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<double>(x1,y1);
auto p2 = complex<double>(x2,y2);
p.drawLine(dataToPixel(p1),dataToPixel(p2));
}
};
constexpr int Lines = 6;
for(int i=0;i<Lines;i++) {
auto angle = (double) i * 30;
constraintLineToCircle(PolarArc(QPointF(0,0), edgeReflection, 0, angle)); // PolarArc
}
for(auto t : traces) {
if(!t.second) {
// trace not enabled in plot
continue;
}
auto trace = t.first;
if(!trace->isVisible()) {
// 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;i<nPoints;i++) {
auto last = trace->sample(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<double>(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();
}
}

View File

@ -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

View File

@ -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<double> 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<double> TraceSmithChart::pixelToData(QPoint p)
{
auto data = transform.inverted().map(QPointF(p));
return complex<double>(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<double>::max();
double closestXpos = 0;
unsigned int closestIndex = 0;
auto samples = t->size();
for(unsigned int i=0;i<samples;i++) {
auto data = t->sample(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<Circles * 2;i++) {
auto radius = (double) i / Circles;
drawArc(SmithChartArc(QPointF(1.0 - radius, 0.0), radius, 0, 2*M_PI));
drawArc(SmithChartArc(QPointF(1.0 + radius, 0.0), radius, 0, 2*M_PI));
drawArc(SmithChartArc(QPointF(1.0 - radius+dx, 0.0), radius, 0, 2*M_PI));
drawArc(SmithChartArc(QPointF(1.0 + radius+dx, 0.0), radius, 0, 2*M_PI));
}
p.drawLine(dataToPixel(complex<double>(edgeReflection,0)),dataToPixel(complex<double>(-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<double>(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) {

View File

@ -1,7 +1,7 @@
#ifndef TRACESMITHCHART_H
#define TRACESMITHCHART_H
#include "traceplot.h"
#include "tracepolar.h"
#include <QPen>
#include <QPainterPath>
@ -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<double> d);
QPoint dataToPixel(Trace::Data d);
std::complex<double> 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<SmithChartConstantLine> constantLines;
};