Files @ 6b388219977e
Branch filter:

Location: tempo-plotter/src/zoomer.cpp - annotation

Hasan Yavuz ÖZDERYA
add assert
bdc33b5c53c6
53e3647e3096
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
c9922f40a20e
c9d4060942ac
e6427508f520
53e3647e3096
e6427508f520
e6427508f520
a05995d9dbe1
c9d4060942ac
262f56d3e063
1e848e98efb7
bdc33b5c53c6
c9d4060942ac
c9d4060942ac
3dcdee629416
3dcdee629416
da3f4cd6026d
da3f4cd6026d
da3f4cd6026d
da3f4cd6026d
da3f4cd6026d
da3f4cd6026d
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
1e848e98efb7
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
bdc33b5c53c6
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
60d04e19e663
d4a271f3d46a
262f56d3e063
262f56d3e063
262f56d3e063
262f56d3e063
262f56d3e063
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
3dcdee629416
e3e70c855836
e3e70c855836
e3e70c855836
e3e70c855836
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
d4a271f3d46a
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
754420135f22
c9d4060942ac
81bc58c2b941
81bc58c2b941
969bab18103b
969bab18103b
969bab18103b
969bab18103b
262f56d3e063
81bc58c2b941
2541096f1ba6
81bc58c2b941
81bc58c2b941
81bc58c2b941
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
2541096f1ba6
2541096f1ba6
dac6c8ef1e56
dac6c8ef1e56
dac6c8ef1e56
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
53e3647e3096
53e3647e3096
53e3647e3096
53e3647e3096
2175b10b9f1e
2541096f1ba6
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
53e3647e3096
2175b10b9f1e
2175b10b9f1e
2541096f1ba6
2541096f1ba6
0c012a246794
0c012a246794
c9922f40a20e
2541096f1ba6
dac6c8ef1e56
2541096f1ba6
2541096f1ba6
c9922f40a20e
2175b10b9f1e
2541096f1ba6
2175b10b9f1e
2175b10b9f1e
262f56d3e063
2175b10b9f1e
c9922f40a20e
2175b10b9f1e
2175b10b9f1e
2175b10b9f1e
c9922f40a20e
2175b10b9f1e
2175b10b9f1e
53e3647e3096
2175b10b9f1e
2175b10b9f1e
2541096f1ba6
2541096f1ba6
2541096f1ba6
2541096f1ba6
2541096f1ba6
2541096f1ba6
2541096f1ba6
969bab18103b
969bab18103b
969bab18103b
969bab18103b
969bab18103b
969bab18103b
e6427508f520
969bab18103b
2541096f1ba6
2541096f1ba6
e6427508f520
e6427508f520
e6427508f520
e6427508f520
a05995d9dbe1
e6427508f520
e6427508f520
e6427508f520
e6427508f520
e6427508f520
e6427508f520
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
4539c1ce7a85
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
4539c1ce7a85
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
c9d4060942ac
/*
  Copyright © 2020 Hasan Yavuz Özderya

  This file is part of serialplot.

  serialplot is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  serialplot is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with serialplot.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "zoomer.h"
#include <qwt_plot.h>
#include <QPen>
#include <QMouseEvent>
#include <QtMath>
#include <algorithm>

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)
{
    is_panning = false;

    setTrackerMode(AlwaysOn);

    // set corner widget between the scrollbars with default background color
    auto cornerWidget = new QWidget();
    auto bgColor = cornerWidget->palette().color(QPalette::Window).name();
    auto styleSheet = QString("background-color:%1;").arg(bgColor);
    cornerWidget->setStyleSheet(styleSheet);
    ScrollZoomer::setCornerWidget(cornerWidget);
}

void Zoomer::zoom(int up)
{
    ScrollZoomer::zoom(up);

    if(zoomRectIndex() == 0)
    {
        emit unzoomed();
    }
}

void Zoomer::zoom( const QRectF & rect)
{
    // set the zoom base when user zooms in to first level
    if (zoomRectIndex() == 0)
    {
        this->setZoomBase(false);
    }

    ScrollZoomer::zoom(rect);
}

void Zoomer::setDispChannels(QVector<const StreamChannel*> channels)
{
    dispChannels = channels;
}

QwtText Zoomer::trackerTextF(const QPointF& pos) const
{
    QwtText b = ScrollZoomer::trackerTextF(pos);

    const QPolygon pa = selection();
    if (!isActive() || pa.count() < 2)
    {
        return b;
    }

    const QRectF rect = invTransform(QRect(pa.first(), pa.last()).normalized());

    QString sizeText = QString(" [%1, %2]").\
        arg(rect.width(), 0, 'g', 4).\
        arg(rect.height(), 0, 'g', 4);

    b.setText(b.text() + sizeText);

    return b;
}

void Zoomer::drawRubberBand(QPainter* painter) const
{
    const double FILL_ALPHA = 0.2;

    QColor color = painter->pen().color();
    color.setAlphaF(FILL_ALPHA);
    painter->setBrush(color);

    ScrollZoomer::drawRubberBand(painter);
}

QRegion Zoomer::rubberBandMask() const
{
    const QPolygon pa = selection();
    if (pa.count() < 2)
    {
        return QRegion();
    }
    const QRect r = QRect(pa.first(), pa.last()).normalized().adjusted(0, 0, 1, 1);
    return QRegion(r);
}

void Zoomer::drawTracker(QPainter* painter) const
{
    if (isActive())
    {
        QwtPlotZoomer::drawTracker(painter);
    }
    else if (dispChannels.length())
    {
        drawValues(painter);
    }
}

QList<const StreamChannel*> Zoomer::visChannels() const
{
    QList<const StreamChannel*> 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<ChannelValue>& values)
{
    typedef ChannelValue LayItem;
    typedef QList<LayItem*> 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<LayGroup*> 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<ChannelValue> 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)
    {
        is_panning = true;
        parentWidget()->setCursor(Qt::ClosedHandCursor);
        pan_point = invTransform(mouseEvent->pos());
    }
    else
    {
        ScrollZoomer::widgetMousePressEvent(mouseEvent);
    }
}

void Zoomer::widgetMouseMoveEvent(QMouseEvent* mouseEvent)
{
    if (is_panning)
    {
        auto cur_point = invTransform(mouseEvent->pos());
        auto delta = cur_point - pan_point;
        moveBy(-delta.x(), -delta.y());
        pan_point = invTransform(mouseEvent->pos());
    }
    else
    {
        ScrollZoomer::widgetMouseMoveEvent(mouseEvent);
    }
}

void Zoomer::widgetMouseReleaseEvent(QMouseEvent* mouseEvent)
{
    if (is_panning)
    {
        is_panning = false;
        parentWidget()->setCursor(Qt::CrossCursor);
    }
    else
    {
        ScrollZoomer::widgetMouseReleaseEvent(mouseEvent);
    }
}