Files
qt5-legatus-legacy/Lts/Common/LtsWidgets/lts_height_plot_widget.cpp
T
2026-08-13 18:33:10 +02:00

628 lines
17 KiB
C++
Executable File

/**
* @RDE_HEADER@
* Widget that visualizes the topography of a bunch of points along a rubberband
* $Id$
* $HeadURL$
* $Author$
* Created by Malte Schroeder, NLSE, 2018-03-07
*/
#include "lts_height_plot_widget.h"
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QTransform>
#include <QMouseEvent>
#include <cmath>
static inline double getLength(const QPointF &p1, const QPointF &p2)
{
const double x = p1.x() - p2.x();
const double y = p1.y() - p2.y();
return sqrt(x * x + y * y);
}
class LtsHeightPlotWidget::Private
{
public:
Private()
: m_length(0)
, m_minElevation(0)
, m_maxElevation(0)
, m_nullX(0)
, m_nullY(0)
, m_scaleX(0)
, m_scaleY(0)
, m_fillBrush(Qt::gray)
, m_axisPen(Qt::black)
, m_mousePosPen(Qt::blue)
, m_markerPen(Qt::magenta)
, iPositionHint(1)
{
m_axisPen.setWidth(1);
m_viewLinePen.setWidth(1);
m_textFont.setPixelSize(10);
m_markerPen.setWidth(2);
m_markerPen.setCapStyle(Qt::RoundCap);
m_mousePosPen.setStyle(Qt::DashDotDotLine);
m_viewLinePen.setStyle(Qt::DashLine);
}
void clearPixmap();
double getDataHeight(double pos) const;
QPointF getDataPos(double pos) const;
int findDataPointIdx(const QPointF &point) const;
void clear();
void recalcData();
void setData(const QVector<QPair<QPointF, double> > &data);
void setMarks(const QVector<QPointF> &data);
void resize(const QSize &newSize);
void repaint();
void paintLines(QPainter &p) const;
QVector< QPair< QPointF, double > > m_data;
QVector< QPointF > m_marks;
QVector< QPair< double, double > > m_heightList; // Laenge, Hoehe
double m_length;
double m_minElevation;
double m_maxElevation;
double m_nullX;
double m_nullY;
double m_scaleX;
double m_scaleY;
QPainterPath m_drawPoints; // PainterPath der Daten, unskaliert
QBrush m_fillBrush;
QPen m_axisPen;
QPen m_viewLinePen;
QPen m_mousePosPen;
QPen m_markerPen;
QFont m_textFont;
QPointF m_mousePos;
QPointF m_mouseLeftClickPos;
QSize m_curSize; // aktuelle Groesse des Widgets
QTransform m_trans1; // nur Verschiebung des Koordinatensystem
QTransform m_trans2; // Translation Pixel <-> Daten
QTransform m_transInverted; // m_trans1.inverted() * m_trans2.inverted()
QTransform m_transCombined; // m_trans2 * m_trans1
QPixmap m_pixmap;
mutable int iPositionHint;
};
double LtsHeightPlotWidget::Private::getDataHeight(double pos) const
{
if (m_heightList.count() <= 1)
return 0;
// boundary checks
if (pos <= 0.)
return m_heightList[0].second;
if (pos >= m_heightList[m_heightList.count() - 1].first)
return m_heightList[m_heightList.count() - 1].second;
if (iPositionHint >= m_heightList.size())
iPositionHint = 1;
if (pos <= m_heightList[iPositionHint].first)
{
while (iPositionHint > 1 && pos <= m_heightList[iPositionHint - 1].first)
--iPositionHint;
}
else
{
while (iPositionHint < m_heightList.count() - 1 && pos > m_heightList[iPositionHint].first)
++iPositionHint;
}
const int iFound = iPositionHint;
// y = m*x + n
const double x = m_heightList[iFound].first - m_heightList[iFound - 1].first;
const double y = m_heightList[iFound].second - m_heightList[iFound - 1].second;
const double m = y / x;
const double n = m_heightList[iFound].second - m * m_heightList[iFound].first;
return m * pos + n;
}
QPointF LtsHeightPlotWidget::Private::getDataPos(double pos) const
{
if (m_heightList.count() <= 1)
return QPointF();
// boundary checks
if (pos <= 0.)
return m_data[0].first;
if (pos >= m_heightList[m_heightList.count() - 1].first)
return m_data[m_heightList.count() - 1].first;
if (pos <= m_heightList[iPositionHint].first)
{
while (iPositionHint > 1 && pos <= m_heightList[iPositionHint - 1].first)
--iPositionHint;
}
else
{
while (iPositionHint < m_heightList.count() - 1 && pos > m_heightList[iPositionHint].first)
++iPositionHint;
}
const int iFound = iPositionHint;
// pos = pos1 + pos1 * prozent(p1,p2)
const double diff = m_heightList[iFound].first - m_heightList[iFound - 1].first;
double percentile = (pos - m_heightList[iFound - 1].first) / diff;
const QPointF &p1 = m_data[iFound - 1].first;
const QPointF p2 = m_data[iFound].first - m_data[iFound - 1].first;
return QPointF(p1.x() + p2.x() * percentile, p1.y() + p2.y() * percentile);
}
int LtsHeightPlotWidget::Private::findDataPointIdx(const QPointF &point) const
{
for (int i = 0; i < m_heightList.count() - 1; ++i)
{
// y = m*x + n
const double x = m_data[i + 1].first.x() - m_data[i].first.x();
const double y = m_data[i + 1].first.y() - m_data[i].first.y();
const double m = y / x;
const double n = m_data[i].first.y() - m * m_data[i].first.x();
// Punkt testen ob er auf der Linie liegt
const double yRes = m * point.x() + n;
if (fabs(yRes - point.y()) < 1.)
{
// und schauen ob er nicht hinter dem zweiten Punkt liegt
const double len = getLength(m_data[ i ].first, point);
if (len <= m_heightList[i].first)
return i;
}
}
return -1;
}
void LtsHeightPlotWidget::Private::clear()
{
m_data.clear();
m_marks.clear();
m_length = 0;
m_minElevation = 0;
m_maxElevation = 0;
m_scaleX = 0;
m_scaleY = 0;
m_drawPoints = QPainterPath();
m_heightList.clear();
m_curSize = QSize();
iPositionHint = 1;
}
void LtsHeightPlotWidget::Private::clearPixmap()
{
m_pixmap = QPixmap();
}
inline double log5(double x)
{
return log10(x) / log10(5.);
}
void LtsHeightPlotWidget::Private::recalcData()
{
m_heightList.clear();
m_drawPoints = QPainterPath();
if (m_data.isEmpty())
{
clear();
return;
}
double length = 0;
double minElevation = m_data.at(0).second;
double maxElevation = minElevation;
for (int i = 1; i < m_data.count(); ++i)
{
length += getLength(m_data.at(i - 1).first, m_data.at(i).first);
minElevation = std::min(minElevation, m_data.at(i).second);
maxElevation = std::max(maxElevation, m_data.at(i).second);
}
// some sanity checks:
if (minElevation < -10000.0) minElevation = -10000.0;
if (maxElevation < -10000.0) maxElevation = -10000.0;
if (minElevation > 10000.0) minElevation = 10000.0;
if (maxElevation > 10000.0) maxElevation = 10000.0;
if (qFuzzyCompare(minElevation, maxElevation))
{
minElevation -= 1.0;
maxElevation += 1.0;
}
// Laenge auf 5^(n-1) m runden
double n2 = pow(5, floor(log5(fabs(minElevation))) - 1);
if (qFuzzyIsNull(n2))
n2 = 0.1; // avoid division by zero
m_minElevation = lrint(floor(minElevation / n2) * n2);
m_maxElevation = lrint(ceil(maxElevation / n2) * n2);
m_scaleY = pow(10, floor(log10(m_maxElevation - m_minElevation)) - 1);
if (m_scaleY < 0.1)
m_scaleY = 0.1;
m_nullY = m_minElevation;
if (m_minElevation < 0 && m_maxElevation > 0)
m_nullY = 0;
// Laenge auf 10^n / 2 m runden
const double n = pow(10, floor(log10(length))) / 2;
m_length = lrint(ceil(length / n) * n);
m_scaleX = pow(10, floor(log10(length)) - 1);
if (m_scaleX < 0.1)
m_scaleX = 0.1;
m_nullX = 0;
double len = 0;
m_drawPoints.moveTo(len, m_data.at(0).second);
m_heightList.append(QPair<double, double>(len, m_data.at(0).second));
for (int i = 1; i < m_data.count(); ++i)
{
len += getLength(m_data.at(i - 1).first, m_data.at(i).first);
m_drawPoints.lineTo(len, m_data.at(i).second);
m_heightList.append(QPair<double, double>(len, m_data.at(i).second));
//fprintf(stderr, "point at len %f\n", len);
}
m_drawPoints.lineTo(len, m_minElevation);
m_drawPoints.lineTo(0, m_minElevation);
m_drawPoints.setFillRule(Qt::WindingFill);
}
void LtsHeightPlotWidget::Private::setData(const QVector< QPair< QPointF, double > > &data)
{
if (data.isEmpty())
{
clear();
return;
}
QSize tmp = m_curSize;
clear();
m_data = data;
recalcData();
resize(tmp);
}
void LtsHeightPlotWidget::Private::setMarks(const QVector< QPointF > &data)
{
m_marks = data;
recalcData();
repaint();
}
void LtsHeightPlotWidget::Private::resize(const QSize &newSize)
{
if (newSize == m_curSize)
return;
m_curSize = newSize;
const int border = 25;
const double width = m_curSize.width();
const double height = m_curSize.height();
const double minY = m_minElevation - m_scaleY;
const double maxY = m_maxElevation + m_scaleY;
const double wertebereichY = maxY - minY;
const double minX = 0 - m_scaleX;
const double maxX = m_length + m_scaleX;
const double wertebereichX = maxX - minX;
m_trans1.reset();
m_trans2.reset();
// 10 Pixel nach rechts und y = 0 ist unten anstatt oben
m_trans1.translate(10, height);
// + und - y tauschen - damit im ersten Quadrant x und y positiv sind
m_trans1.scale(1, -1);
// und noch etwas vom Rand weg
m_trans1.translate(border, border);
// und wieder zurueck
m_trans1.scale(1, -1);
// die aktuelle Breite und Hoehe relativ zu der Breite und Hoehe der Daten
m_trans2.scale((width - 2 * border) / (wertebereichX),
(height - 2 * border) / (wertebereichY));
// m_minElevation ist unser '0-Wert', minY demnach der kleinste y-Wert
m_trans2.translate(0, minY);
// + und - y tauschen - damit im ersten Quadrant x und y positiv sind
m_trans2.scale(1, -1);
m_transInverted = m_trans1.inverted() * m_trans2.inverted();
m_transCombined = m_trans2 * m_trans1;
repaint();
}
void LtsHeightPlotWidget::Private::repaint()
{
if (!m_curSize.isValid())
return;
if (!m_heightList.count())
return;
QImage img(m_curSize, QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPainter p(&img);
const QPainter::RenderHints hints = QPainter::HighQualityAntialiasing |
QPainter::SmoothPixmapTransform |
QPainter::TextAntialiasing |
QPainter::Antialiasing;
p.setRenderHints(hints);
p.setTransform(m_trans1);
// Daten zeichnen
p.setPen(QPen(m_fillBrush, 0.1));
p.setBrush(m_fillBrush);
p.drawPath(m_trans2.map(m_drawPoints));
p.setRenderHint(QPainter::Antialiasing, false);
// Marker zeichnen
p.setPen(m_markerPen);
for (int i = 0; i < m_marks.count(); ++i)
{
int posIdx = findDataPointIdx(m_marks.at(i));
if (posIdx < 0)
continue;
double len = getLength(m_data.at(posIdx).first, m_marks.at(i));
len += m_heightList.at(posIdx).first;
const double height = getDataHeight(len);
const QLineF l(len, m_minElevation, len, height);
//fprintf(stderr, "marker at len %f\n", len);
p.drawLine(m_trans2.map(l));
}
// Koordinatensystem + Beschriftung
p.setPen(m_axisPen);
p.setFont(m_textFont);
// Y-Achse
p.drawLine(m_trans2.map(QPointF(m_nullX, m_minElevation - m_scaleY)),
m_trans2.map(QPointF(m_nullX, m_maxElevation + m_scaleY)));
// kleine Striche
for (double i = m_minElevation; i <= m_maxElevation; i += m_scaleY)
{
const qreal y = m_trans2.map(QPointF(0, i)).y();
p.drawLine(QLineF(-2, y, 2, y));
}
// grosse Striche
for (double i = m_minElevation; i <= m_maxElevation; i += m_scaleY * 5)
{
const qreal y = m_trans2.map(QPointF(0, i)).y();
p.drawLine(QLineF(-4, y, 4, y));
}
QFontMetricsF fm(m_textFont);
for (double i = m_minElevation; i <= m_maxElevation; i += m_scaleY * 5)
{
if (qFuzzyCompare(i, m_nullY))
continue;
const qreal y = m_trans2.map(QPointF(m_nullX, i)).y();
const QPointF p1(-20, y);
const QString str = QString::number(i);
QRectF r = fm.boundingRect(str);
r.setWidth(fm.width(str)); // important, see docu!
r.moveTo(p1.x() - r.width() / 2., p1.y() - r.height() / 2.);
p.drawText(r, str, Qt::AlignHCenter | Qt::AlignVCenter);
}
// X-Achse
p.drawLine(m_trans2.map(QPointF(-m_scaleX, m_nullY)),
m_trans2.map(QPointF(m_scaleX + m_length, m_nullY)));
// kleine Striche
for (double i = m_scaleX; i <= m_length; i += m_scaleX)
{
const QPointF point = m_trans2.map(QPointF(i, m_nullY));
p.drawLine(QLineF(point.x(), point.y() - 2, point.x(), point.y() + 2));
}
// grosse Striche
for (double i = 0; i <= m_length; i += m_scaleX * 5)
{
const QPointF point = m_trans2.map(QPointF(i, m_nullY));
p.drawLine(QLineF(point.x(), point.y() - 4, point.x(), point.y() + 4));
}
for (double i = 0; i <= m_length; i += m_scaleX * 5)
{
if (qFuzzyCompare(i, m_nullX))
continue;
const QPointF point = m_trans2.map(QPointF(i, m_nullY));
const QPointF p1(point.x(), point.y() + 15);
const QString str = QString::number(i);
QRectF r = fm.boundingRect(str);
r.setWidth(fm.width(str)); // important, see docu!
r.moveTo(p1.x() - r.width() / 2., p1.y() - r.height() / 2.);
p.drawText(r, str, Qt::AlignHCenter | Qt::AlignVCenter);
}
p.end();
m_pixmap = QPixmap::fromImage(img);
}
void LtsHeightPlotWidget::Private::paintLines(QPainter &p) const
{
if (m_heightList.count() > 0)
{
// Mouse-Position zeichnen
QPointF pos = m_mousePos;
if (pos.x() < 0)
pos.setX(0);
if (pos.x() > m_heightList.at(m_heightList.count() - 1).first)
pos.setX(m_heightList.at(m_heightList.count() - 1).first);
//keine Transformation setzen da sonst die Linienstaerke des Pens veraendert wird!
//p.setTransform(d->m_transCombined);
const double curHeight = getDataHeight(pos.x());
p.setPen(m_mousePosPen);
p.drawLine(m_transCombined.map(QLineF(m_nullX, curHeight, pos.x(), curHeight)));
p.drawLine(m_transCombined.map(QLineF(pos.x(), m_nullY, pos.x(), curHeight)));
// Sichtlinie zeichnen
if (!m_mouseLeftClickPos.isNull())
{
const double y = curHeight > pos.y() ? curHeight : pos.y();
p.drawLine(m_transCombined.map(QLineF(m_nullX, y, pos.x(), y)));
p.drawLine(m_transCombined.map(QLineF(pos.x(), m_nullY, pos.x(), y)));
p.setPen(m_viewLinePen);
const QPointF &c = m_mouseLeftClickPos;
p.drawLine(m_transCombined.map(QLineF(m_nullX, c.y(), c.x(), c.y())));
p.drawLine(m_transCombined.map(QLineF(c.x(), m_nullY, c.x(), c.y())));
// nur hier da die anderen beiden Linien sonst nicht schoen aussehen
const QLineF line(m_mouseLeftClickPos, QPointF(pos.x(), y));
p.drawLine(m_transCombined.map(line));
}
}
}
LtsHeightPlotWidget::LtsHeightPlotWidget(QWidget *parent)
: QWidget(parent)
, d(new Private)
{
setMinimumSize(300, 300);
resize(size());
setMouseTracking(true);
}
LtsHeightPlotWidget::~LtsHeightPlotWidget()
{
delete d;
}
void LtsHeightPlotWidget::clearView()
{
d->clearPixmap();
update();
}
void LtsHeightPlotWidget::setData(const QVector< QPair< QPointF, double > > &data)
{
d->setData(data);
update();
}
void LtsHeightPlotWidget::setMarks(const QVector<QPointF> &data)
{
d->setMarks(data);
}
void LtsHeightPlotWidget::setBackgroundBrush(const QBrush &brush)
{
QPalette pal = palette();
pal.setBrush(QPalette::Background, brush);
setPalette(pal);
}
void LtsHeightPlotWidget::setTextFont(const QFont &f)
{
d->m_textFont = f;
d->repaint();
}
void LtsHeightPlotWidget::setAxisPen(const QPen &pen)
{
d->m_axisPen = pen;
d->repaint();
}
void LtsHeightPlotWidget::setPositionPen(const QPen &pen)
{
d->m_mousePosPen = pen;
}
void LtsHeightPlotWidget::setViewLinePen(const QPen &pen)
{
d->m_viewLinePen = pen;
}
void LtsHeightPlotWidget::setContourBrush(const QBrush &brush)
{
d->m_fillBrush = brush;
d->repaint();
}
void LtsHeightPlotWidget::setMarkerPen(const QPen &pen)
{
d->m_markerPen = pen;
d->repaint();
}
void LtsHeightPlotWidget::resizeEvent(QResizeEvent *e)
{
d->resize(size());
QWidget::resizeEvent(e);
}
void LtsHeightPlotWidget::paintEvent(QPaintEvent *e)
{
Q_UNUSED(e);
QPainter p(this);
const QPainter::RenderHints hints = QPainter::SmoothPixmapTransform |
QPainter::TextAntialiasing |
QPainter::Antialiasing;
p.setRenderHints(hints);
// vorgefertiges Bild zeichnen
p.drawPixmap(0, 0, d->m_pixmap);
// und jetzt die Hilfslinien
d->paintLines(p);
p.end();
}
void LtsHeightPlotWidget::mouseMoveEvent(QMouseEvent *e)
{
QWidget::mouseMoveEvent(e);
if (!d->m_heightList.count())
return;
const QPointF pos = d->m_transInverted.map(e->localPos());
emit mousePositionChanged(d->getDataPos(pos.x()), d->getDataHeight(pos.x()), pos.y());
d->m_mousePos = pos;
update();
}
void LtsHeightPlotWidget::mouseReleaseEvent(QMouseEvent *e)
{
QWidget::mouseReleaseEvent(e);
if (!d->m_heightList.count())
return;
if (e->button() == Qt::LeftButton)
{
const QPointF &pos = d->m_mousePos;
if (pos.x() < 0)
return;
if (pos.x() > d->m_heightList.at(d->m_heightList.count() - 1).first)
return;
const double curHeight = d->getDataHeight(pos.x());
d->m_mouseLeftClickPos = QPointF(pos.x(), curHeight > pos.y() ? curHeight : pos.y());
emit startLineOfSight(d->getDataPos(pos.x()));
}
else if (e->button() == Qt::RightButton)
{
d->m_mouseLeftClickPos = QPointF();
emit stopLineOfSight();
update();
}
}