diff --git a/src/zoomer.cpp b/src/zoomer.cpp --- a/src/zoomer.cpp +++ b/src/zoomer.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2020 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,9 +19,13 @@ #include "zoomer.h" #include -#include +#include +#include +#include +#include -#include +static const int VALUE_POINT_DIAM = 4; +static const int VALUE_TEXT_MARGIN = VALUE_POINT_DIAM + 2; Zoomer::Zoomer(QWidget* widget, bool doReplot) : ScrollZoomer(widget) @@ -59,6 +63,11 @@ void Zoomer::zoom( const QRectF & rect) ScrollZoomer::zoom(rect); } +void Zoomer::setDispChannels(QVector channels) +{ + dispChannels = channels; +} + QwtText Zoomer::trackerTextF(const QPointF& pos) const { QwtText b = ScrollZoomer::trackerTextF(pos); @@ -102,6 +111,225 @@ QRegion Zoomer::rubberBandMask() const return QRegion(r); } +void Zoomer::drawTracker(QPainter* painter) const +{ + if (isActive()) + { + QwtPlotZoomer::drawTracker(painter); + } + else if (dispChannels.length()) + { + drawValues(painter); + } +} + +QList Zoomer::visChannels() const +{ + QList result; + + for (unsigned ci = 0; ci < (unsigned) dispChannels.length(); ci++) + { + if (dispChannels[ci]->visible()) + result.append(dispChannels[ci]); + } + + return result; +} + +const double ValueLabelHeight = 12; // TODO: calculate + +struct ChannelValue +{ + const StreamChannel* ch; + double value; + double y; + double top() const {return y;}; + double bottom() const {return y + ValueLabelHeight;}; +}; + +static void layoutValues(QList& values) +{ + typedef ChannelValue LayItem; + typedef QList LayItemList; + + struct LayGroup + { + struct VRange {double top, bottom;}; + LayItemList items; + LayGroup(LayItem* initialItem) {items.append(initialItem);} + unsigned numItems() const {return items.size();} + double top() const {return items.first()->top();} + double bottom() const {return items.last()->bottom();} + VRange vRange() const {return {top(), bottom()};} + double overlap(const LayGroup* otrGroup) const + { + auto myr = vRange(); + auto otr = otrGroup->vRange(); + + double a = myr.bottom - otr.top; + double b = otr.bottom - myr.top; + if (a > 0 and b > 0) + { + return std::min(a, b); + } + return 0; + } + void moveBy(double y) {for (auto it : items) it->y += y;} + void join(LayGroup* other) + { + // assumes other group is below this one and they overlap + double ovr_h = overlap(other); + + // groups are moved less if they have more items and vice versa + double ratio = double(numItems()) / double(numItems() + other->numItems()); + double self_off = ovr_h * (1. - ratio); + + // make sure we don't go out of screen (above) after the shift + double final_top = top() - self_off; + if (final_top < 0) + self_off += final_top; + + // move groups + moveBy(-self_off); // up + other->moveBy(ovr_h - self_off); // down + + // finalize the merge by gettin items from other + do + { + items.append(other->items.takeFirst()); + } while (!other->items.isEmpty()); + } + }; + + // create initial groups (1 group per item) + QList groups; + for (auto& val : values) + groups.append(new LayGroup(&val)); + + // sort groups according to their items position + struct { + bool operator()(LayGroup* a, LayGroup* b) const + { + return a->top() < b->top(); + } + } compTops; + + std::sort(groups.begin(), groups.end(), compTops); + + // do spacing + bool somethingOverlaps = true; + while (somethingOverlaps and groups.size() > 1) + { + somethingOverlaps = false; + for (int i = 0; i < groups.size() - 1; i++) + { + auto a = groups[i]; + auto b = groups[i + 1]; + + // make sure nothing is over the top + if (a->top() < 0) + a->moveBy(-a->top()); + + // join if groups overlap + if (a->overlap(b)) + { + somethingOverlaps = true; + a->join(b); + delete groups.takeAt(i + 1); + break; + } + } + } + + // cleanup + do + { + delete groups.takeFirst(); + } while (!groups.isEmpty()); +}; + +void Zoomer::drawValues(QPainter* painter) const +{ + auto tpos = trackerPosition(); + if (tpos.x() < 0) return; // cursor not on window + + // find Y values for current cursor X position + double x = invTransform(tpos).x(); + auto channels = visChannels(); + QList values; + for (auto ch : channels) + { + double value = ch->findValue(x); + if (!std::isnan(value)) + { + auto point = transform(QPointF(x, value)); + values.append({ch, value, double(point.y())}); + } + } + + // TODO should keep? + if (values.isEmpty()) + { + return; + } + + layoutValues(values); + + painter->save(); + + // draw vertical line + auto linePen = rubberBandPen(); + linePen.setStyle(Qt::DotLine); + painter->setPen(linePen); + const QRect pRect = pickArea().boundingRect().toRect(); + int px = tpos.x(); + painter->drawLine(px, pRect.top(), px, pRect.bottom()); + + // draw sample values + for (auto value : values) + { + double val = value.value; + auto ch = value.ch; + + auto point = transform(QPointF(x, val)); + + painter->setBrush(ch->color()); + painter->setPen(Qt::NoPen); + painter->drawEllipse(point, VALUE_POINT_DIAM, VALUE_POINT_DIAM); + + painter->setPen(rubberBandPen()); + // We give a very small (1x1) rectangle but disable clipping + painter->drawText(QRectF(point.x() + VALUE_TEXT_MARGIN, value.y, 1, 1), + Qt::AlignVCenter | Qt::TextDontClip, + QString("%1").arg(val)); + } + + painter->restore(); +} + +QRect Zoomer::trackerRect(const QFont& font) const +{ + if (isActive()) + { + return QwtPlotZoomer::trackerRect(font); + } + else + { + return valueTrackerRect(font); + } +} + +QRect Zoomer::valueTrackerRect(const QFont& font) const +{ + // TODO: consider using actual tracker values for width calculation + const int textWidth = qCeil(QwtText("-8.8888888").textSize(font).width()); + const int width = textWidth + VALUE_POINT_DIAM + VALUE_TEXT_MARGIN; + const int x = trackerPosition().x() - VALUE_POINT_DIAM; + const auto pickRect = pickArea().boundingRect(); + + return QRect(x, pickRect.y(), width, pickRect.height()); +} + void Zoomer::widgetMousePressEvent(QMouseEvent* mouseEvent) { if (mouseEvent->modifiers() & Qt::ControlModifier)