diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,7 @@ add_executable(${PROGRAM_NAME} WIN32 src/ringbuffer.cpp src/ringbuffer.cpp src/indexbuffer.cpp + src/linindexbuffer.cpp src/readonlybuffer.cpp src/framebufferseries.cpp src/numberformatbox.cpp diff --git a/src/framebuffer.h b/src/framebuffer.h --- a/src/framebuffer.h +++ b/src/framebuffer.h @@ -61,4 +61,24 @@ class WFrameBuffer : public ResizableBuf virtual void clear() = 0; }; +/** + * Abstract base class for X buffers. + * + * These buffers only contain increasing or equal (to previous) values. + */ +class XFrameBuffer : public ResizableBuffer +{ +public: + enum Index {OUT_OF_RANGE = -1}; + + /** + * Finds index for given value. + * + * If given value is bigger than max or smaller than minimum + * returns `OUT_OF_RANGE`. If it's in between values, smaller + * index is returned (not closer one). + */ + virtual int findIndex(double value) const = 0; +}; + #endif // FRAMEBUFFER_H diff --git a/src/framebufferseries.cpp b/src/framebufferseries.cpp --- a/src/framebufferseries.cpp +++ b/src/framebufferseries.cpp @@ -20,75 +20,64 @@ #include #include "framebufferseries.h" -FrameBufferSeries::FrameBufferSeries(const FrameBuffer* buffer) +FrameBufferSeries::FrameBufferSeries(const XFrameBuffer* x, const FrameBuffer* y) { - xAsIndex = true; - _xmin = 0; - _xmax = 1; - _buffer = buffer; + _x = x; + _y = y; + int_index_start = 0; - int_index_end = _buffer->size(); + int_index_end = _y->size(); } -void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax) +void FrameBufferSeries::setX(const XFrameBuffer* x) { - xAsIndex = asIndex; - _xmin = xmin; - _xmax = xmax; + _x = x; } size_t FrameBufferSeries::size() const { - return int_index_end - int_index_start; + return int_index_end - int_index_start + 1; } QPointF FrameBufferSeries::sample(size_t i) const { i += int_index_start; - if (xAsIndex) - { - return QPointF(i, _buffer->sample(i)); - } - else - { - return QPointF(i * (_xmax - _xmin) / _buffer->size() + _xmin, _buffer->sample(i)); - } + return QPointF(_x->sample(i), _y->sample(i)); } QRectF FrameBufferSeries::boundingRect() const { QRectF rect; - auto yLim = _buffer->limits(); + auto yLim = _y->limits(); + auto xLim = _x->limits(); rect.setBottom(yLim.start); rect.setTop(yLim.end); - if (xAsIndex) - { - rect.setLeft(0); - rect.setRight(size()); - } - else - { - rect.setLeft(_xmin); - rect.setRight(_xmax); - } + rect.setLeft(xLim.start); + rect.setRight(xLim.end); + return rect.normalized(); } void FrameBufferSeries::setRectOfInterest(const QRectF& rect) { - if (xAsIndex) + int_index_start = _x->findIndex(rect.left()); + int_index_end = _x->findIndex(rect.right()); + + if (int_index_start == XFrameBuffer::OUT_OF_RANGE) { - int_index_start = floor(rect.left())-1; - int_index_end = ceil(rect.right())+1; + int_index_start = 0; } - else + else if (int_index_start > 0) { - double xsize = _xmax - _xmin; - size_t bsize = _buffer->size(); - int_index_start = floor(bsize * (rect.left()-_xmin) / xsize)-1; - int_index_end = ceil(bsize * (rect.right()-_xmin) / xsize)+1; + int_index_start -= 1; } - int_index_start = std::max(int_index_start, 0); - int_index_end = std::min((int) _buffer->size(), int_index_end); + if (int_index_end == XFrameBuffer::OUT_OF_RANGE) + { + int_index_end = _x->size()-1; + } + else if (int_index_end < (int)_x->size()-1) + { + int_index_end += 1; + } } diff --git a/src/framebufferseries.h b/src/framebufferseries.h --- a/src/framebufferseries.h +++ b/src/framebufferseries.h @@ -35,10 +35,9 @@ class FrameBufferSeries : public QwtSeriesData { public: - FrameBufferSeries(const FrameBuffer* buffer); + FrameBufferSeries(const XFrameBuffer* x, const FrameBuffer* y); - /// Behavior of X axis - void setXAxis(bool asIndex, double xmin, double xmax); + void setX(const XFrameBuffer* x); // QwtSeriesData implementations size_t size() const; @@ -47,10 +46,8 @@ public: void setRectOfInterest(const QRectF& rect); private: - const FrameBuffer* _buffer; - bool xAsIndex; - double _xmin; - double _xmax; + const XFrameBuffer* _x; + const FrameBuffer* _y; int int_index_start; ///< starting index of "rectangle of interest" int int_index_end; ///< ending index of "rectangle of interest" diff --git a/src/indexbuffer.cpp b/src/indexbuffer.cpp --- a/src/indexbuffer.cpp +++ b/src/indexbuffer.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -47,3 +47,15 @@ Range IndexBuffer::limits() const { return Range{0, _size-1.}; } + +int IndexBuffer::findIndex(double value) const +{ + if (value < 0 || value > size() - 1) + { + return OUT_OF_RANGE; + } + else + { + return value; + } +} diff --git a/src/indexbuffer.h b/src/indexbuffer.h --- a/src/indexbuffer.h +++ b/src/indexbuffer.h @@ -26,15 +26,16 @@ /// sample value. /// /// @note This buffer isn't for storing data. -class IndexBuffer : public ResizableBuffer +class IndexBuffer : public XFrameBuffer { public: IndexBuffer(unsigned n); - unsigned size() const; - double sample(unsigned i) const; - Range limits() const; - void resize(unsigned n); + unsigned size() const override; + double sample(unsigned i) const override; + Range limits() const override; + void resize(unsigned n) override; + int findIndex(double value) const override; private: unsigned _size; diff --git a/src/linindexbuffer.cpp b/src/linindexbuffer.cpp --- a/src/linindexbuffer.cpp +++ b/src/linindexbuffer.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -18,12 +18,14 @@ */ #include +#include #include "linindexbuffer.h" LinIndexBuffer::LinIndexBuffer(unsigned n, Range lim) { - Q_ASSERT(n > 0); + // Note that calculation of _step would cause divide by 0 + Q_ASSERT(n > 1); _size = n; setLimits(lim); @@ -50,6 +52,18 @@ void LinIndexBuffer::resize(unsigned n) setLimits(_limits); // called to update `_step` } +int LinIndexBuffer::findIndex(double value) const +{ + if (value < _limits.start || value > _limits.end) + { + return OUT_OF_RANGE; + } + + int r = (value - _limits.start) / _step; + // Note: we are limiting return value because of floating point in-accuracies + return std::min(std::max(r, 0), (_size-1)); +} + void LinIndexBuffer::setLimits(Range lim) { _limits = lim; diff --git a/src/linindexbuffer.h b/src/linindexbuffer.h --- a/src/linindexbuffer.h +++ b/src/linindexbuffer.h @@ -26,17 +26,19 @@ /// intermediate values are calculated linearly. /// /// @note This buffer isn't for storing data. -class LinIndexBuffer : public ResizableBuffer +class LinIndexBuffer : public XFrameBuffer { public: LinIndexBuffer(unsigned n, Range lim); LinIndexBuffer(unsigned n, double min, double max) : LinIndexBuffer(n, {min, max}) {}; - unsigned size() const; - double sample(unsigned i) const; - Range limits() const; - void resize(unsigned n); + unsigned size() const override; + double sample(unsigned i) const override; + Range limits() const override; + void resize(unsigned n) override; + int findIndex(double value) const override; + /// Sets minimum and maximum sample values of the buffer. void setLimits(Range lim); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -169,6 +169,9 @@ MainWindow::MainWindow(QWidget *parent) plotMan, &PlotManager::setYAxis); connect(&plotControlPanel, &PlotControlPanel::xScaleChanged, + &stream, &Stream::setXAxis); + + connect(&plotControlPanel, &PlotControlPanel::xScaleChanged, plotMan, &PlotManager::setXAxis); connect(&plotControlPanel, &PlotControlPanel::plotWidthChanged, @@ -215,6 +218,9 @@ MainWindow::MainWindow(QWidget *parent) plotControlPanel.setChannelInfoModel(stream.infoModel()); // init scales + stream.setXAxis(plotControlPanel.xAxisAsIndex(), + plotControlPanel.xMin(), plotControlPanel.xMax()); + plotMan->setYAxis(plotControlPanel.autoScale(), plotControlPanel.yMin(), plotControlPanel.yMax()); plotMan->setXAxis(plotControlPanel.xAxisAsIndex(), diff --git a/src/plot.cpp b/src/plot.cpp --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -91,6 +91,11 @@ Plot::~Plot() if (snapshotOverlay != NULL) delete snapshotOverlay; } +void Plot::setDispChannels(QVector channels) +{ + zoomer.setDispChannels(channels); +} + void Plot::setYAxis(bool autoScaled, double yAxisMin, double yAxisMax) { this->isAutoScaled = autoScaled; diff --git a/src/plot.h b/src/plot.h --- a/src/plot.h +++ b/src/plot.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -50,6 +50,9 @@ public: Plot(QWidget* parent = 0); ~Plot(); + /// Set displayed channels for value tracking (can be null) + void setDispChannels(QVector channels); + public slots: void showGrid(bool show = true); void showMinorGrid(bool show = true); diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp --- a/src/plotmanager.cpp +++ b/src/plotmanager.cpp @@ -19,7 +19,6 @@ #include #include -#include #include "qwt_symbol.h" #include "plot.h" @@ -31,9 +30,9 @@ PlotManager::PlotManager(QWidget* plotAr const Stream* stream, QObject* parent) : QObject(parent) { + _stream = stream; construct(plotArea, menu); - _stream = stream; - if (_stream == NULL) return; + if (_stream == nullptr) return; // connect to ChannelInfoModel infoModel = _stream->infoModel(); @@ -53,15 +52,15 @@ PlotManager::PlotManager(QWidget* plotAr // add initial curves if any? for (unsigned int i = 0; i < stream->numChannels(); i++) { - addCurve(stream->channel(i)->name(), stream->channel(i)->yData()); + addCurve(stream->channel(i)->name(), stream->channel(i)->xData(), stream->channel(i)->yData()); } - } PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu, Snapshot* snapshot, QObject *parent) : QObject(parent) { + _stream = nullptr; construct(plotArea, menu); setNumOfSamples(snapshot->numSamples()); @@ -70,7 +69,7 @@ PlotManager::PlotManager(QWidget* plotAr for (unsigned ci = 0; ci < snapshot->numChannels(); ci++) { - addCurve(snapshot->channelName(ci), snapshot->yData[ci]); + addCurve(snapshot->channelName(ci), snapshot->xData[ci], snapshot->yData[ci]); } connect(infoModel, &QAbstractItemModel::dataChanged, @@ -94,8 +93,6 @@ void PlotManager::construct(QWidget* plo // initalize layout and single widget isMulti = false; scrollArea = NULL; - setupLayout(isMulti); - addPlotWidget(); // connect to menu connect(menu, &PlotMenu::symbolShowChanged, this, &PlotManager:: setSymbols); @@ -148,7 +145,7 @@ void PlotManager::onNumChannelsChanged(u // add new channels for (unsigned int i = oldNum; i < numOfChannels; i++) { - addCurve(_stream->channel(i)->name(), _stream->channel(i)->yData()); + addCurve(_stream->channel(i)->name(), _stream->channel(i)->xData(), _stream->channel(i)->yData()); } } else if(numOfChannels < oldNum) @@ -217,8 +214,6 @@ void PlotManager::checkNoVisChannels() void PlotManager::setMulti(bool enabled) { - if (enabled == isMulti) return; - isMulti = enabled; // detach all curves @@ -239,11 +234,14 @@ void PlotManager::setMulti(bool enabled) if (isMulti) { // add new widgets and attach + int i = 0; for (auto curve : curves) { auto plot = addPlotWidget(); plot->setVisible(curve->isVisible()); + plot->setDispChannels(QVector(1, _stream->channel(i))); curve->attach(plot); + i++; } } else @@ -251,6 +249,11 @@ void PlotManager::setMulti(bool enabled) // add a single widget auto plot = addPlotWidget(); + if (_stream != nullptr) + { + plot->setDispChannels(_stream->allChannels()); + } + // attach all curves for (auto curve : curves) { @@ -332,11 +335,10 @@ Plot* PlotManager::addPlotWidget() return plot; } -void PlotManager::addCurve(QString title, const FrameBuffer* buffer) +void PlotManager::addCurve(QString title, const XFrameBuffer* xBuf, const FrameBuffer* yBuf) { auto curve = new QwtPlotCurve(title); - auto series = new FrameBufferSeries(buffer); - series->setXAxis(_xAxisAsIndex, _xMin, _xMax); + auto series = new FrameBufferSeries(xBuf, yBuf); curve->setSamples(series); _addCurve(curve); } @@ -356,10 +358,12 @@ void PlotManager::_addCurve(QwtPlotCurve { // create a new plot widget plot = addPlotWidget(); + plot->setDispChannels(QVector(1, _stream->channel(index))); } else { plot = plotWidgets[0]; + plot->setDispChannels(_stream->allChannels()); } // show the curve @@ -481,10 +485,13 @@ void PlotManager::setXAxis(bool asIndex, _xAxisAsIndex = asIndex; _xMin = xMin; _xMax = xMax; + + int ci = 0; for (auto curve : curves) { FrameBufferSeries* series = static_cast(curve->data()); - series->setXAxis(asIndex, xMin, xMax); + series->setX(_stream->channel(ci)->xData()); + ci++; } for (auto plot : plotWidgets) { diff --git a/src/plotmanager.h b/src/plotmanager.h --- a/src/plotmanager.h +++ b/src/plotmanager.h @@ -41,7 +41,7 @@ class PlotManager : public QObject public: explicit PlotManager(QWidget* plotArea, PlotMenu* menu, - const Stream* stream = NULL, + const Stream* stream = nullptr, QObject *parent = 0); explicit PlotManager(QWidget* plotArea, PlotMenu* menu, Snapshot* snapshot, @@ -49,7 +49,7 @@ public: ~PlotManager(); /// Add a new curve with title and buffer. A color is /// automatically chosen for curve. - void addCurve(QString title, const FrameBuffer* buffer); + void addCurve(QString title, const XFrameBuffer* xBuf, const FrameBuffer* yBuf); /// Removes curves from the end void removeCurves(unsigned number); /// Returns current number of curves known by plot manager @@ -82,7 +82,7 @@ private: QList curves; QList plotWidgets; Plot* emptyPlot; ///< for displaying when all channels are hidden - const Stream* _stream; ///< attached stream, can be `NULL` + const Stream* _stream; ///< attached stream, can be `nullptr` const ChannelInfoModel* infoModel; bool isDemoShown; bool _autoScaled; diff --git a/src/snapshot.h b/src/snapshot.h --- a/src/snapshot.h +++ b/src/snapshot.h @@ -28,6 +28,7 @@ #include "channelinfomodel.h" #include "readonlybuffer.h" +#include "indexbuffer.h" class SnapshotView; class MainWindow; @@ -40,7 +41,8 @@ public: Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved = false); ~Snapshot(); - // TODO: yData of snapshot shouldn't be public, preferable should be handled in constructor + // TODO: yData and xData of snapshot shouldn't be public, preferable should be handled in constructor + QVector xData; QVector yData; QAction* showAction(); QAction* deleteAction(); diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp --- a/src/snapshotmanager.cpp +++ b/src/snapshotmanager.cpp @@ -71,6 +71,7 @@ Snapshot* SnapshotManager::makeSnapshot( for (unsigned ci = 0; ci < _stream->numChannels(); ci++) { + snapshot->xData.append(new IndexBuffer(_stream->numSamples())); snapshot->yData.append(new ReadOnlyBuffer(_stream->channel(ci)->yData())); } @@ -194,6 +195,7 @@ void SnapshotManager::loadSnapshotFromFi for (unsigned ci = 0; ci < numOfChannels; ci++) { + snapshot->xData.append(new IndexBuffer(data[ci].size())); snapshot->yData.append(new ReadOnlyBuffer(data[ci].data(), data[ci].size())); } diff --git a/src/stream.cpp b/src/stream.cpp --- a/src/stream.cpp +++ b/src/stream.cpp @@ -20,6 +20,7 @@ #include "stream.h" #include "ringbuffer.h" #include "indexbuffer.h" +#include "linindexbuffer.h" Stream::Stream(unsigned nc, bool x, unsigned ns) : _infoModel(nc) @@ -27,15 +28,20 @@ Stream::Stream(unsigned nc, bool x, unsi _numSamples = ns; _paused = false; + xAsIndex = true; + xMin = 0; + xMax = 1; + // create xdata buffer _hasx = x; if (x) { - xData = new RingBuffer(ns); + // TODO: implement XRingBuffer (binary search) + Q_ASSERT(false); } else { - xData = new IndexBuffer(ns); + xData = makeXBuffer(); } // create channels @@ -81,6 +87,16 @@ StreamChannel* Stream::channel(unsigned return const_cast(static_cast(*this).channel(index)); } +QVector Stream::allChannels() const +{ + QVector result(numChannels()); + for (unsigned ci = 0; ci < numChannels(); ci++) + { + result[ci] = channel(ci); + } + return result; +} + const ChannelInfoModel* Stream::infoModel() const { return &_infoModel; @@ -118,11 +134,12 @@ void Stream::setNumChannels(unsigned nc, { if (x) { - xData = new RingBuffer(_numSamples); + // TODO: implement XRingBuffer (binary search) + Q_ASSERT(false); } else { - xData = new IndexBuffer(_numSamples); + xData = makeXBuffer(); } for (auto c : channels) @@ -142,6 +159,18 @@ void Stream::setNumChannels(unsigned nc, Sink::setNumChannels(nc, x); } +XFrameBuffer* Stream::makeXBuffer() const +{ + if (xAsIndex) + { + return new IndexBuffer(_numSamples); + } + else + { + return new LinIndexBuffer(_numSamples, xMin, xMax); + } +} + const SamplePack* Stream::applyGainOffset(const SamplePack& pack) const { Q_ASSERT(infoModel()->gainOrOffsetEn()); @@ -191,7 +220,9 @@ void Stream::feedIn(const SamplePack& pa unsigned ns = pack.numSamples(); if (_hasx) { - static_cast(xData)->addSamples(pack.xData(), ns); + // TODO: implement XRingBuffer (binary search) + Q_ASSERT(false); + // static_cast(xData)->addSamples(pack.xData(), ns); } // modified pack that gain and offset is applied to @@ -237,6 +268,24 @@ void Stream::setNumSamples(unsigned valu } } +void Stream::setXAxis(bool asIndex, double min, double max) +{ + xAsIndex = asIndex; + xMin = min; + xMax = max; + + // Note that x axis scaling is ignored when X is provided from source as data + // TODO: assert (UI options for x axis should be disabled) + if (!hasX()) + { + xData = makeXBuffer(); + for (auto c : channels) + { + c->setX(xData); + } + } +} + void Stream::saveSettings(QSettings* settings) const { _infoModel.saveSettings(settings); diff --git a/src/stream.h b/src/stream.h --- a/src/stream.h +++ b/src/stream.h @@ -48,16 +48,16 @@ public: * @param x has X data input * @param ns number of samples */ - Stream(unsigned nc = 0, bool x = false, unsigned ns = 0); + Stream(unsigned nc = 1, bool x = false, unsigned ns = 2); ~Stream(); - // implementations for `Source` - virtual bool hasX() const; - virtual unsigned numChannels() const; + bool hasX() const; + unsigned numChannels() const; unsigned numSamples() const; const StreamChannel* channel(unsigned index) const; StreamChannel* channel(unsigned index); + QVector allChannels() const; const ChannelInfoModel* infoModel() const; ChannelInfoModel* infoModel(); @@ -79,10 +79,13 @@ signals: void dataAdded(); ///< emitted when data added to channel man. public slots: - // TODO: these won't be public - // void setNumChannels(unsigned number); + /// Change number of samples (buffer size) void setNumSamples(unsigned value); + /// Change X axis style + /// @note Ignored when X is provided by source (hasX == true) + void setXAxis(bool asIndex, double min, double max); + /// When paused data feed is ignored void pause(bool paused); @@ -94,11 +97,14 @@ private: bool _paused; bool _hasx; - ResizableBuffer* xData; + XFrameBuffer* xData; QList channels; ChannelInfoModel _infoModel; + bool xAsIndex; + double xMin, xMax; + /** * Applies gain and offset to given pack. * @@ -111,6 +117,9 @@ private: * @return modified data */ const SamplePack* applyGainOffset(const SamplePack& pack) const; + + /// Returns a new virtual X buffer for settings + XFrameBuffer* makeXBuffer() const; }; diff --git a/src/streamchannel.cpp b/src/streamchannel.cpp --- a/src/streamchannel.cpp +++ b/src/streamchannel.cpp @@ -17,9 +17,10 @@ along with serialplot. If not, see . */ +#include #include "streamchannel.h" -StreamChannel::StreamChannel(unsigned i, const FrameBuffer* x, +StreamChannel::StreamChannel(unsigned i, const XFrameBuffer* x, FrameBuffer* y, ChannelInfoModel* info) { _index = i; @@ -37,8 +38,37 @@ unsigned StreamChannel::index() const {r QString StreamChannel::name() const {return _info->name(_index);}; QColor StreamChannel::color() const {return _info->color(_index);}; bool StreamChannel::visible() const {return _info->isVisible(_index);}; -const FrameBuffer* StreamChannel::xData() const {return _x;} +const XFrameBuffer* StreamChannel::xData() const {return _x;} const FrameBuffer* StreamChannel::yData() const {return _y;} FrameBuffer* StreamChannel::yData() {return _y;} const ChannelInfoModel* StreamChannel::info() const {return _info;} -void StreamChannel::setX(const FrameBuffer* x) {_x = x;}; +void StreamChannel::setX(const XFrameBuffer* x) {_x = x;}; + +double StreamChannel::findValue(double x) const +{ + int index = _x->findIndex(x); + Q_ASSERT(index < (int) _x->size()); + + if (index >= 0) + { + // can't do estimation for last sample + if (index == (int) _x->size() - 1) + { + return _y->sample(index); + } + else + { + // calculate middle of the line + double prev_x = _x->sample(index); + double next_x = _x->sample(index+1); + double ratio = (x - prev_x) / (next_x - prev_x); + double prev_y = _y->sample(index); + double next_y = _y->sample(index+1); + return ratio * (next_y - prev_y) + prev_y; + } + } + else + { + return std::numeric_limits::quiet_NaN(); + } +} diff --git a/src/streamchannel.h b/src/streamchannel.h --- a/src/streamchannel.h +++ b/src/streamchannel.h @@ -35,7 +35,7 @@ public: * @param info channel info model */ StreamChannel(unsigned i, - const FrameBuffer* x, + const XFrameBuffer* x, FrameBuffer* y, ChannelInfoModel* info); ~StreamChannel(); @@ -44,15 +44,23 @@ public: QString name() const; QColor color() const; bool visible() const; - const FrameBuffer* xData() const; + const XFrameBuffer* xData() const; FrameBuffer* yData(); const FrameBuffer* yData() const; const ChannelInfoModel* info() const; - void setX(const FrameBuffer* x); + void setX(const XFrameBuffer* x); + + /** + * Returns sample value for `x`. + * + * If `x` is out of range `NaN` is returned. A calculated (linear) + * value is returned when `x` is in between two data points. + */ + double findValue(double x) const; private: unsigned _index; - const FrameBuffer* _x; + const XFrameBuffer* _x; FrameBuffer* _y; ChannelInfoModel* _info; }; 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 © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -19,9 +19,12 @@ #include "zoomer.h" #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 +62,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 +110,96 @@ QRegion Zoomer::rubberBandMask() const return QRegion(r); } +void Zoomer::drawTracker(QPainter* painter) const +{ + if (isActive()) + { + QwtPlotZoomer::drawTracker(painter); + } + else if (dispChannels.length()) + { + drawValues(painter); + } +} + +void Zoomer::drawValues(QPainter* painter) const +{ + painter->save(); + + double x = invTransform(trackerPosition()).x(); + auto values = findValues(x); + + // draw vertical line + auto linePen = rubberBandPen(); + linePen.setStyle(Qt::DotLine); + painter->setPen(linePen); + const QRect pRect = pickArea().boundingRect().toRect(); + int px = trackerPosition().x(); + painter->drawLine(px, pRect.top(), px, pRect.bottom()); + + // draw sample values + for (int ci = 0; ci < values.size(); ci++) + { + if (!dispChannels[ci]->visible()) continue; + + double val = values[ci]; + if (!std::isnan(val)) + { + auto p = transform(QPointF(x, val)); + + painter->setBrush(dispChannels[ci]->color()); + painter->setPen(Qt::NoPen); + painter->drawEllipse(p, VALUE_POINT_DIAM, VALUE_POINT_DIAM); + + painter->setPen(rubberBandPen()); + // We give a very small (1x1) rectangle but disable clipping + painter->drawText(QRectF(p.x() + VALUE_TEXT_MARGIN, p.y(), 1, 1), + Qt::AlignVCenter | Qt::TextDontClip, + QString("%1").arg(val)); + } + } + + painter->restore(); +} + +QVector Zoomer::findValues(double x) const +{ + unsigned nc = dispChannels.length(); + QVector result(nc); + for (unsigned ci = 0; ci < nc; ci++) + { + if (dispChannels[ci]->visible()) + { + result[ci] = dispChannels[ci]->findValue(x); + } + + } + return result; +} + +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) diff --git a/src/zoomer.h b/src/zoomer.h --- a/src/zoomer.h +++ b/src/zoomer.h @@ -1,5 +1,5 @@ /* - Copyright © 2017 Hasan Yavuz Özderya + Copyright © 2018 Hasan Yavuz Özderya This file is part of serialplot. @@ -20,25 +20,35 @@ #ifndef ZOOMER_H #define ZOOMER_H -#include +#include +#include + +#include "scrollzoomer.h" +#include "streamchannel.h" class Zoomer : public ScrollZoomer { Q_OBJECT public: - Zoomer(QWidget *, bool doReplot=true); + Zoomer(QWidget*, bool doReplot=true); void zoom(int up); - void zoom( const QRectF & ); + void zoom(const QRectF&); + /// Set displayed channels for value tracking (can be null) + void setDispChannels(QVector channels); signals: void unzoomed(); protected: /// Re-implemented to display selection size in the tracker text. - QwtText trackerTextF(const QPointF &pos) const; + QwtText trackerTextF(const QPointF &pos) const override; + /// Re-implemented for sample value tracker + QRect trackerRect(const QFont&) const override; /// Re-implemented for alpha background - void drawRubberBand(QPainter* painter) const; + void drawRubberBand(QPainter* painter) const override; + /// Re-implemented to draw sample values + void drawTracker(QPainter* painter) const override; /// Re-implemented for alpha background (masking is basically disabled) QRegion rubberBandMask() const; /// Overloaded for panning @@ -51,6 +61,15 @@ protected: private: bool is_panning; QPointF pan_point; + /// displayed channels for value tracking + QVector dispChannels; + + /// Draw sample values + void drawValues(QPainter* painter) const; + /// Find sample values for given X value + QVector findValues(double x) const; + /// Returns trackerRect for value tracker + QRect valueTrackerRect(const QFont& font) const; }; #endif // ZOOMER_H diff --git a/tests/test.cpp b/tests/test.cpp --- a/tests/test.cpp +++ b/tests/test.cpp @@ -221,6 +221,14 @@ TEST_CASE("LinIndexBuffer", "[memory, bu REQUIRE(buf.sample(0) == -5.0); REQUIRE(buf.sample(19) == 5.0); + + buf.resize(10); + buf.setLimits({0., 3.}); + REQUIRE(buf.findIndex(0.01) == 0); + REQUIRE(buf.findIndex(1.51) == 4); + REQUIRE(buf.findIndex(2.99) == 8); + REQUIRE(buf.findIndex(3.01) == XFrameBuffer::OUT_OF_RANGE); + REQUIRE(buf.findIndex(-0.01) == XFrameBuffer::OUT_OF_RANGE); } TEST_CASE("RingBuffer sizing", "[memory, buffer]") diff --git a/tests/test_stream.cpp b/tests/test_stream.cpp --- a/tests/test_stream.cpp +++ b/tests/test_stream.cpp @@ -27,17 +27,17 @@ TEST_CASE("construction of stream with d // default values are an empty stream with no channels Stream s; - REQUIRE(s.numChannels() == 0); + REQUIRE(s.numChannels() == 1); REQUIRE(!s.hasX()); - REQUIRE(s.numSamples() == 0); + REQUIRE(s.numSamples() == 2); } TEST_CASE("construction of stream with parameters", "[memory, stream]") { - Stream s(4, true, 100); + Stream s(4, false, 100); REQUIRE(s.numChannels() == 4); - REQUIRE(s.hasX()); + REQUIRE(!s.hasX()); REQUIRE(s.numSamples() == 100); for (unsigned i = 0; i < 4; i++) @@ -64,6 +64,8 @@ TEST_CASE("changing stream number of cha REQUIRE(c->index() == i); } +// TODO: enable test when `Stream` supports X channel +#if 0 // increase nc value, add X so._setNumChannels(5, true); @@ -76,6 +78,7 @@ TEST_CASE("changing stream number of cha REQUIRE(c != NULL); REQUIRE(c->index() == i); } +#endif // reduce nc value, remove X so._setNumChannels(1, false); @@ -127,9 +130,11 @@ TEST_CASE("adding data to a stream with } } +// TODO: enable test when `Stream` supports X channel +#if 0 TEST_CASE("adding data to a stream with X", "[memory, stream, data, sink]") { - Stream s(3, true, 10); + Stream s(3, false, 10); // prepare data SamplePack pack(5, 3, true); @@ -147,7 +152,7 @@ TEST_CASE("adding data to a stream with } TestSource so(3, true); - so.connectSink(&s); + REQUIRE_THROWS(so.connectSink(&s)); // test so._feed(pack); @@ -178,6 +183,7 @@ TEST_CASE("adding data to a stream with REQUIRE(x->sample(i) == (i-5)+10); } } +#endif TEST_CASE("paused stream shouldn't store data", "[memory, stream, pause]") {