diff --git a/Makefile b/Makefile index 34eec35..9237cbf 100644 --- a/Makefile +++ b/Makefile @@ -16,16 +16,19 @@ MKDIR = mkdir CP = copy RM = del /Q RMDIR = rmdir /S /Q +CPPFLAGS += -mwindows +run_demo = $(target_demo) else MKDIR = mkdir -p CP = cp RM = rm -f RMDIR = rm -f -r +run_demo = ./$(target_demo) endif cpp_options = -I$(includedir) `pkg-config gtkmm-3.0 --cflags --libs` -latomic $(CPPFLAGS) headers = $(foreach h, $(wildcard *.h), $(includedir_subdir)/$(h)) -objects = circularbuffer.o plottingarea.o recorder.o frontend.o +objects = circularbuffer.o plotarea.o recorder.o frontend.o $(target): $(headers) $(objects) $(libdir) $(AR) rcs $@ $(objects) @@ -46,9 +49,9 @@ $(includedir_subdir)/%.h: $(includedir_subdir) %.h $(CP) $(@F) $< demo: $(target_demo) - ./$< + $(run_demo) .PHONY: clean clean: -$(RMDIR) lib include - -$(RM) *.o target_demo + -$(RM) *.o $(target_demo) diff --git a/README.md b/README.md index 04b2f53..dd22783 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ make demo ``` You can modify `demo.cpp` to change the wave form and make other adjustments, like speed, buffer size or axis-y range. -Install: +## Install ``` sudo make -e prefix=/usr ``` @@ -22,25 +22,40 @@ For 32-bit Windows: 2. Download `msys2-i686-latest.sfx.exe`, place it under a short path and extract it; 3. Follow the instruments in . +Add `/bin` to environment variable `PATH`, then MSYS2-compiled programs can be executed outside MSYS2 shell. Add linker flag `-mwindows` to hide the console window. + ## Classes -`AxisRange`: Closed range between two `float` values. It supports many operations, including mapping of a given value to another range. All of it's functions are inlined. +### ValueRange, IndexRange +Closed range between two values. It supports many operations, including mapping of a given value to another range. All of it's functions are inlined. `ValueRange` is implemented by two `float` variables, while `IndexRange` is implemented by two `unsigned long int` variables. Implicit conversions between them are supported. + +### CircularBuffer +Where the data should be pushed back to update the graph in `PlottingArea`. After it becomes full, it discards an item each time a new item is pushed into, but it avoids moving every item in the memory region. It's functions are thread-safe, and most of its simple functions are inlined. + +Optimized algorithms calculating min/max/average values are implemented here, and spike detection is enabled by default so that spikes can be treated specially to avoid flickering of spikes when the x-axis index step for data plotting is adjusted for a wide index range. -`CircularBuffer`: Where the data should be pushed back to update the graph in `PlottingArea`. After it becomes full, it discards an item each time a new item is pushed into, but it avoids moving every item in the memory region. Optimized algorithms calculating min/max/average values are implemented here, and spike detection is enabled by default so that spikes can be treated specially to avoid flickering of spikes when the x-axis index step for data plotting is adjusted for a wide index range. It is thread-safe, and most of its simple functions are inlined. +### PlotArea +Implements a graph box for a single buffer without scroll box. It only supports a single variable, and values should be pushed into its buffer manually. Axis ranges can be set either automatically or manually, and the grid with tick values can be either fixed or auto-adjusted. For x-axis index range, goto-end mode and extend mode are available. -`PlottingArea`: Implements a graph box for a single buffer without scroll box. It only supports a single variable, and values should be pushed into its buffer manually. Axis ranges can be set either automatically or manually, and the grid with tick values can be either fixed or auto-adjusted. For x-axis index range, goto-end mode and extend mode are available. +Notice: `PlotArea` cannot receive button press event and button release event by itself. If needed, put it inside a `Gtk::EventBox` which handles these events. -`VariableAccessPtr`: Pointer of an variable or a function which has a `void*` parameter and returns a `float` value. Pointer of a member function of class `T` which returns a `float` value and has no extra parameters can be created by: +### VariableAccessPtr +Pointer of an variable or a function which has a `void*` parameter and returns a `float` value. Pointer of a member function of class `T` which returns a `float` value and has no extra parameters can be created by: ``` MemberFuncPtr(T* pobj) ``` -`Recorder`: Packs multiple plotting areas and a scroll box for x-axis. It accepts a group of `VariableAccessPtr` pointers from which the data is read, then creates multiple buffers multiple plotting areas for these variables. After it is started, it reads and pushs the data into the buffers in given interval, and the unit of axis-x values is set to seconds. Provides zoom in/out (by left/right mouse button clicking on it) and CSV file opening/saving features. +### Recorder +Packs multiple plotting areas and a scroll box for x-axis. It accepts a group of `VariableAccessPtr` pointers from which the data is read, then creates multiple buffers multiple plotting areas for these variables. After it is started, it reads and pushs the data into the buffers in given interval, and the unit of axis-x values is set to seconds. It provides zoom in/out (by left/right mouse button clicking on it) and CSV file opening/saving features. + +`Recorder` is not capable of loading a block of data at once. In this case, it can still be used to show data (alias `RecordView` can be used for this purpose). Do not call `Recorder::start()`, but load data into each buffer manually, then call `Recorder::refresh_view()`. Call `Recorder::clear()` to clear them. In case of the amount of data in the buffers are not equal, that of the buffer for the first variable makes sense. To avoid writing invalid non-zero data into the CSV file, call `CircularBuffer::erase()` for each buffer after calling `Recorder::clear()`. + +### Frontend +Provides a simplest interface to create a Gtk application and a window for the recorder. Call its member function `open()` to create a new thread for Gtk, then it will run until it is destructed or its member function `close()` is called; call `run()` to join the Gtk thread, then it will run until the window is closed. -`Frontend`: Provides a simple interface to create a Gtk application and a window for the recorder. It runs in a seperate thread for Gtk, until it is destructed or its member function `close()` is being called. Call its member function `run()` to join the Gtk thread, then it will run until the window is closed. Notice: Through function `recorder()` you can get a reference of the `Recorder` object as a Gtk Widget after the window is opened, but the object itself will be destructed when the window is being closed. +Notice: +1. Another `Gtk::Application` cannot be created after the `Frontend` has opened, and running multiple `Frontend` is not possible. +2. Through function `recorder()` you can get a reference of the `Recorder` object as a Gtk widget after the window is opened, but the object itself will be destructed when the window is being closed. ## Known Issues -1. On Windows, segmentation fault will be produced when the thread created by `Frontend` exits. Use `Frontend::run()` instead of `Frontend::open()` (which creates a new thread for `Gtk::Application::run()`) on Windows, especially if you need to do necessary things after the window is closed by the user. -2. Fast refresh rate (25~50 Hz for example) of the graph may cause heavy CPU load (at least on a computer without GPU): it seems that processing of signal `draw` costs CPU even if nothing is to be drawn in `on_draw()`, and it becomes serious when the graph area is expanded (with window maximized). Keeping a Cairo context in `PlottingArea` instead of emitting the signal `draw` which creates and provides the Cairo context for drawing might work, but is probably error-prone because that violates the GTK drawing model, and `Gdk::Window::create_cairo_context()` is deprecated long ago. -3. The record process can be interrupted by the environment, this causes missing of data and unsmooth curves on the graph. It works very well on XFCE, and is acceptable on GNOME and KDE, but the unsmooth effect can be significant on Windows that the delay can sometimes exceed 20 ms. Limited by software timer accuracy, it is IMPOSSIBLE for `Recorder` to keep its data sampling frequency higher than 10 kHz (0.1 ms interval). The higher the frequency, the lower the stability. -4. `Recorder` is not capable of loading a block of data at once. In this case, it can still be used to show data: do not call `Recorder::start()`, but load data into each buffer manually, then call `Recorder::refresh_view()` to refresh the graph. In case of the amount of data in the buffers are not equal, that of the buffer for the first variable makes sense. To avoid writing invalid non-zero data into the CSV file, call `CircularBuffer::erase()` for each buffer after calling `Recorder::clear()`. -5. It has not been migrated to `gtkmm-4.0`, partly because newest distributions of Debian and Ubuntu has not provided this version of the library. +1. The record process of `Recorder` can be interrupted by the environment, this causes missing of data and unsmooth curves on the graph. It works well on XFCE, and is acceptable on GNOME and KDE, but the unsmooth effect can be significant on Windows that the delay can sometimes exceed 20 ms. Limited by software timer accuracy, it is IMPOSSIBLE for `Recorder` to keep its data sampling frequency higher than 10 kHz (0.1 ms interval). The higher the frequency, the lower the stability. +2. It has not been migrated to `gtkmm-4.0`, partly because newest distributions of Debian and Ubuntu has not provided this version of the library. diff --git a/axisrange.h b/axisrange.h index 413c279..fdfae5c 100644 --- a/axisrange.h +++ b/axisrange.h @@ -9,119 +9,174 @@ namespace SimpleCairoPlot { +class ValueRange; class AxisValues; +class IndexRange; -class AxisRange //closed range -{ - float val_min, val_max; - float val_length; +using Range = ValueRange; +using AxisRange = ValueRange; +using UIntRange = IndexRange; +class ValueRange //closed range +{ public: - AxisRange(float min, float max); + ValueRange(float min, float max); float min() const; float max() const; float length() const; float center() const; - bool operator==(const AxisRange& range) const; - bool operator!=(const AxisRange& range) const; + bool operator==(const ValueRange& range) const; + bool operator!=(const ValueRange& range) const; bool contain(float val) const; - bool contain(AxisRange range) const; - bool intersected_not_left_of(AxisRange range) const; + bool contain(ValueRange range) const; float fit_value(float val) const; - AxisRange cut_range(AxisRange range) const; - AxisRange fit_range(AxisRange range) const; + ValueRange cut_range(ValueRange range) const; + ValueRange fit_range(ValueRange range) const; - float map(float val, float target_width, bool reverse = false) const; - float map(float val, AxisRange range, bool reverse = false) const; - float map_reverse(float val, float target_width) const; - float map_reverse(float val, AxisRange range) const; + float map(float val, ValueRange range, bool reverse = false) const; + float map_reverse(float val, ValueRange range) const; void set(float min, float max); - void move(float offset); void min_move_to(float min); //max moves with min void max_move_to(float max); //min moves with max - - void fit_by_range(AxisRange range); - + void fit_by_range(ValueRange range); void scale(float factor, float cursor); void scale(float factor); - void set_int(); +private: + float val_min, val_max; + float val_length; +}; + +class AxisValues +{ +public: + AxisValues(ValueRange range, unsigned int divider, bool adjust = true); + unsigned int count() const; + float operator[](unsigned int i) const; + +private: + enum {Cnt_Choices = 5}; + const float Choices[Cnt_Choices] = {1, 2, 2.5, 5, 10}; + + float val_first, cell_width; + unsigned int cnt; +}; + +class IndexRange +{ +public: + IndexRange(); + IndexRange(unsigned long int min, unsigned long int max); + IndexRange(const ValueRange& ax); + + operator ValueRange() const; + ValueRange to_axis(long int offset = 0) const; + + operator bool() const; + unsigned long int min() const; + unsigned long int max() const; + unsigned long int count() const; + unsigned long int length() const; + unsigned long int count_by_step(unsigned int step) const; + + bool operator==(const IndexRange& range) const; + bool operator!=(const IndexRange& range) const; + + bool contain(unsigned long int i) const; + bool contain(IndexRange range) const; + bool intersected_not_left_of(IndexRange range) const; + unsigned long int fit_index(unsigned long int index) const; + unsigned long int fit_value(unsigned long int val) const; //same as fit_index() + IndexRange cut_range(IndexRange range) const; + IndexRange fit_range(IndexRange range) const; + + float map(unsigned long int val, ValueRange range, bool reverse = false) const; + float map_reverse(unsigned long int val, ValueRange range) const; + + void set(unsigned long int min, unsigned long int max); + void move(long int offset); + void min_move_to(unsigned long int min); //min moves with max + void max_move_to(unsigned long int max); //max moves with min + void fit_by_range(IndexRange range); + void step_align_with(IndexRange range, unsigned int step); + +private: + bool valid; + unsigned long int val_min, val_max, cnt; }; -using Range = AxisRange; +long int subtract(unsigned long int a, unsigned long int b); +IndexRange intersection(IndexRange range1, IndexRange range2); + +/*------------------------------ ValueRange functions ------------------------------*/ -inline AxisRange::AxisRange(float min, float max) +inline ValueRange::ValueRange(float min, float max) { this->set(min, max); } -inline float AxisRange::min() const +inline float ValueRange::min() const { return this->val_min; } -inline float AxisRange::max() const +inline float ValueRange::max() const { return this->val_max; } -inline float AxisRange::length() const +inline float ValueRange::length() const { return this->val_length; } -inline float AxisRange::center() const +inline float ValueRange::center() const { return (this->val_min + this->val_max) / 2.0; } -inline bool AxisRange::operator==(const AxisRange& range) const +inline bool ValueRange::operator==(const ValueRange& range) const { return this->val_min == range.min() && this->val_max == range.max(); } -inline bool AxisRange::operator!=(const AxisRange& range) const +inline bool ValueRange::operator!=(const ValueRange& range) const { return this->val_min != range.min() || this->val_max != range.max(); } -inline bool AxisRange::contain(float val) const +inline bool ValueRange::contain(float val) const { return this->val_min <= val && val <= this->val_max; } -inline bool AxisRange::contain(AxisRange range) const +inline bool ValueRange::contain(ValueRange range) const { return this->val_min <= range.min() && range.max() <= this->val_max; } -inline bool AxisRange::intersected_not_left_of(AxisRange range) const -{ - return range.contain(this->val_min) && this->contain(range.max()); -} - -inline float AxisRange::fit_value(float val) const +inline float ValueRange::fit_value(float val) const { if (val < this->val_min) return this->val_min; if (val > this->val_max) return this->val_max; return val; } -inline AxisRange AxisRange::cut_range(AxisRange range) const +inline ValueRange ValueRange::cut_range(ValueRange range) const { - return AxisRange(this->fit_value(range.min()), this->fit_value(range.max())); + return ValueRange(this->fit_value(range.min()), this->fit_value(range.max())); } -inline AxisRange AxisRange::fit_range(AxisRange range) const +inline ValueRange ValueRange::fit_range(ValueRange range) const { if (this->contain(range)) return range; if (range.length() >= this->val_length) return *this; - AxisRange range_new = range; + ValueRange range_new = range; if (range.min() < this->val_min) range_new.min_move_to(this->val_min); else if (range.max() > this->val_max) @@ -130,32 +185,22 @@ inline AxisRange AxisRange::fit_range(AxisRange range) const return range_new; } -inline float AxisRange::map(float val, float target_width, bool reverse) const +inline float ValueRange::map(float val, ValueRange range, bool reverse) const { if (this->val_length == 0) return 0; - val = fit_value(val); //ensures that val is valid - val = (target_width * (val - this->val_min)) / this->val_length; - if (reverse) val = target_width - val; - return val; -} - -inline float AxisRange::map(float val, AxisRange range, bool reverse) const -{ - return this->map(val, range.length(), reverse) + range.min(); -} - -inline float AxisRange::map_reverse(float val, float target_width) const -{ - return this->map(val, target_width, true); + val = this->fit_value(val); //ensures that val is valid + val = (range.length() * (val - this->val_min)) / this->val_length; + if (reverse) val = range.length() - val; + val += range.min(); return val; } -inline float AxisRange::map_reverse(float val, AxisRange range) const +inline float ValueRange::map_reverse(float val, ValueRange range) const { return this->map(val, range, true); } -inline void AxisRange::set(float min, float max) +inline void ValueRange::set(float min, float max) { if (min <= max) { this->val_min = min; this->val_max = max; @@ -166,32 +211,27 @@ inline void AxisRange::set(float min, float max) this->val_length = this->val_max - this->val_min; } -inline void AxisRange::move(float offset) +inline void ValueRange::move(float offset) { this->val_min += offset; this->val_max += offset; } -inline void AxisRange::min_move_to(float min) +inline void ValueRange::min_move_to(float min) { - float offset = min - this->val_min; - this->val_min = min; - this->val_max += offset; + this->move(min - this->val_min); } -inline void AxisRange::max_move_to(float max) +inline void ValueRange::max_move_to(float max) { - float offset = max - this->val_max; - this->val_max = max; - this->val_min += offset; + this->move(max - this->val_max); } -inline void AxisRange::fit_by_range(AxisRange range) +inline void ValueRange::fit_by_range(ValueRange range) { - AxisRange range_new = range.fit_range(*this); - this->set(range_new.min(), range_new.max()); + *this = range.fit_range(*this); } -inline void AxisRange::scale(float factor, float cursor) +inline void ValueRange::scale(float factor, float cursor) { if (factor < 0) factor = -factor; @@ -200,33 +240,14 @@ inline void AxisRange::scale(float factor, float cursor) this->set(cursor - l, cursor + r); } -inline void AxisRange::scale(float factor) +inline void ValueRange::scale(float factor) { this->scale(factor, this->center()); } -inline void AxisRange::set_int() -{ - using namespace std; - this->val_min = round(this->val_min); - this->val_max = round(this->val_max); -} - -class AxisValues -{ - enum {Cnt_Choices = 5}; - const float Choices[Cnt_Choices] = {1, 2, 2.5, 5, 10}; - - float val_first, cell_width; - unsigned int cnt; +/*------------------------------ AxisValues functions ------------------------------*/ -public: - AxisValues(AxisRange range, unsigned int divider, bool adjust = true); - unsigned int count() const; - float operator[](unsigned int i) const; -}; - -inline AxisValues::AxisValues(AxisRange range, unsigned int divider, bool adjust) +inline AxisValues::AxisValues(ValueRange range, unsigned int divider, bool adjust) { using namespace std; if (divider == 0) divider = 1; @@ -267,6 +288,229 @@ inline float AxisValues::operator[](unsigned int i) const return this->val_first + i*this->cell_width; } +/*------------------------------ IndexRange functions ------------------------------*/ + +inline long int subtract(unsigned long int a, unsigned long int b) +{ + if (a >= b) + return a - b; + else + return -(long int)(b - a); +} + +inline IndexRange intersection(IndexRange range1, IndexRange range2) +{ + return range1.cut_range(range2); +} + +inline IndexRange::IndexRange() +{ + this->valid = false; + this->cnt = this->val_min = this->val_max = 0; +} + +inline IndexRange::IndexRange(unsigned long int min, unsigned long int max) +{ + this->set(min, max); +} + +inline IndexRange::IndexRange(const ValueRange& ax) +{ + if (ax.max() < 0) { + this->cnt = this->val_min = this->val_max = 0; + this->valid = false; return; + } + + using namespace std; + float min = ax.min(); if (min < 0) min = 0; + this->set(round(min), round(ax.max())); +} + +inline IndexRange::operator ValueRange() const +{ + return this->to_axis(); +} + +inline ValueRange IndexRange::to_axis(long int offset) const +{ + if (! this->valid) return ValueRange(-1, -1); + return ValueRange((float)this->val_min + offset, + (float)this->val_max + offset); +} + +inline IndexRange::operator bool() const +{ + return this->valid; +} + +inline unsigned long int IndexRange::min() const +{ + return this->val_min; +} + +inline unsigned long int IndexRange::max() const +{ + return this->val_max; +} + +inline unsigned long int IndexRange::count() const +{ + return this->cnt; +} + +inline unsigned long int IndexRange::length() const +{ + if (! this->valid) return 0; + return this->count() - 1; +} + +inline unsigned long int IndexRange::count_by_step(unsigned int step) const +{ + if (!this->valid || step == 0) return 0; + unsigned int quo = this->count() / step, rem = this->count() % step; + if (rem > 0) quo++; return quo; +} + +inline bool IndexRange::operator==(const IndexRange& range) const +{ + if (!this->valid && !range.valid) return true; + return this->valid && range.valid + && this->val_min == range.min() && this->val_max == range.max(); +} + +inline bool IndexRange::operator!=(const IndexRange& range) const +{ + return !(*this == range); +} + +inline bool IndexRange::contain(unsigned long int i) const +{ + return this->valid && this->val_min <= i && i <= this->val_max; +} + +inline bool IndexRange::contain(IndexRange range) const +{ + return this->valid && range.valid + && this->val_min <= range.val_min && range.val_max <= this->val_max; +} + +inline bool IndexRange::intersected_not_left_of(IndexRange range) const +{ + if (!this->valid && !range.valid) return false; + return range.contain(this->val_min) && this->contain(range.max()); +} + +inline unsigned long int IndexRange::fit_index(unsigned long int index) const +{ + if (! this->valid) return 0; + if (index < this->val_min) return this->val_min; + if (index > this->val_max) return this->val_max; + return index; +} + +inline unsigned long int IndexRange::fit_value(unsigned long int val) const +{ + return this->fit_index(val); +} + +inline IndexRange IndexRange::cut_range(IndexRange range) const +{ + if (!this->valid || !range.valid) return IndexRange(); + if (!this->contain(range.min()) && !this->contain(range.max()) && !range.contain(*this)) + return IndexRange(); + return IndexRange(this->fit_index(range.min()), this->fit_index(range.max())); +} + +inline IndexRange IndexRange::fit_range(IndexRange range) const +{ + if (!this->valid || !range.valid) return IndexRange(); + if (this->contain(range)) return range; + if (range.count() >= this->cnt) return *this; + + IndexRange range_new = range; + if (range.min() < this->val_min) + range_new.min_move_to(this->val_min); + else if (range.max() > this->val_max) + range_new.max_move_to(this->val_max); + + return range_new; +} + +inline float IndexRange::map(unsigned long int val, ValueRange range, bool reverse) const +{ + val = this->fit_value(val); + return ValueRange(0, this->length()).map(val - this->val_min, range, reverse); +} + +inline float IndexRange::map_reverse(unsigned long int val, ValueRange range) const +{ + return this->map(val, range, true); +} + +inline void IndexRange::set(unsigned long int min, unsigned long int max) +{ + if (max < min) { + this->cnt = this->val_min = this->val_max = 0; + this->valid = false; return; + } + + this->val_min = min; this->val_max = max; + this->cnt = max - min + 1; + this->valid = true; +} + +inline void IndexRange::move(long int offset) +{ + if (! this->valid) return; + if (offset < -(long long int)this->val_min) + offset = -(long long int)this->val_min; + this->val_min += offset; this->val_max += offset; //unsigned long int + long int works +} + +inline void IndexRange::min_move_to(unsigned long int min) +{ + // note: signed - unsigned is dangerous + if (! this->valid) return; + this->move(subtract(min, this->val_min)); +} + +inline void IndexRange::max_move_to(unsigned long int max) +{ + if (! this->valid) return; + this->move(subtract(max, this->val_max)); +} + +inline void IndexRange::fit_by_range(IndexRange range) +{ + *this = range.fit_range(*this); +} + +inline void IndexRange::step_align_with(IndexRange range, unsigned int step) +{ + if (! range) return; + + // note: signed *,/,<,> unsigned is dangerous + long int diff_min = subtract(this->min(), range.min()); + diff_min = round(diff_min / (long int)step) * (long int)step; + + unsigned long int new_min, new_max; + + if (diff_min >= 0 || -diff_min < (long long int)range.min()) + new_min = range.min() + diff_min; + else + new_min = 0; + + new_max = new_min + this->count_by_step(step)*step; + while (new_max > this->max()) { + if (new_max >= step) + new_max -= step; + else + new_max = 0; + } + + this->set(new_min, new_max); +} + } #endif diff --git a/circularbuffer.cpp b/circularbuffer.cpp index 5b6a352..c562ffd 100644 --- a/circularbuffer.cpp +++ b/circularbuffer.cpp @@ -63,18 +63,13 @@ void CircularBuffer::copy_from(const CircularBuffer& from) unsigned int cnt_cpy = from.cnt; //actual amount of data to be copied if (cnt_cpy > this->bufsize) cnt_cpy = this->bufsize; + IndexRange range_cpy(from.count() - cnt_cpy, from.count() - 1); - const float* pf1, * pf2 = NULL; //copy from two segments in from.buf - unsigned int cnt_f1, cnt_f2; - pf1 = from.item_addr(from.cnt - cnt_cpy); - if (pf1 + cnt_cpy - 1 > from.bufend) { - cnt_f1 = from.bufend - pf1 + 1; - pf2 = from.buf; cnt_f2 = cnt_cpy - cnt_f1; - } else - cnt_f1 = cnt_cpy; - - memcpy(this->buf, pf1, cnt_f1*sizeof(float)); - if (pf2) memcpy(this->buf + cnt_f1, pf2, cnt_f2*sizeof(float)); + BufRangeMap map = from.map_from(range_cpy); + memcpy(this->buf, from.buf + map.former.min(), map.former.count()*sizeof(float)); + if (map.latter) + memcpy(this->buf + map.former.count(), from.buf + map.latter.min(), + map.latter.count()*sizeof(float)); this->cnt = cnt_cpy; this->end = this->ptr_inc(this->buf, cnt_cpy); @@ -166,15 +161,11 @@ void CircularBuffer::load(const float* data, unsigned int cnt, bool spike_check) this->push(*pf, true, false); this->cnt_overwrite += cnt - cnt_load; } else { - float* p1 = this->end, * p2 = NULL; //two segments of the current buffer - unsigned int cnt1, cnt2; - if (p1 + cnt_load - 1 > this->bufend) { - cnt1 = this->bufend - p1 + 1; - p2 = this->buf; cnt2 = cnt_load - cnt1; - } else - cnt1 = cnt_load; - memcpy(p1, pf, cnt1*sizeof(float)); - if (p2) memcpy(p2, pf + cnt1, cnt2*sizeof(float)); + BufRangeMap map = this->map_from(IndexRange(0, cnt_load - 1)); + memcpy(this->buf + map.former.min(), pf, map.former.count()*sizeof(float)); + if (map.latter) + memcpy(this->buf + map.latter.min(), pf + map.former.count(), + map.latter.count()*sizeof(float)); unsigned long int tmp_cnt = this->cnt + cnt; if (tmp_cnt > this->bufsize) { @@ -187,7 +178,7 @@ void CircularBuffer::load(const float* data, unsigned int cnt, bool spike_check) this->unlock(); } -unsigned int CircularBuffer::get_spikes(Range range, unsigned int* buf_out) +unsigned int CircularBuffer::get_spikes(IndexRange range, unsigned int* buf_out) { if (this->buf_spike_cnt == 0) return 0; @@ -209,7 +200,7 @@ unsigned int CircularBuffer::get_spikes(Range range, unsigned int* buf_out) return cnt_sp; } -unsigned int CircularBuffer::get_spikes(Range range, unsigned long int* buf_out) +unsigned int CircularBuffer::get_spikes(IndexRange range, unsigned long int* buf_out) { if (this->buf_spike_cnt == 0) return 0; @@ -230,11 +221,11 @@ unsigned int CircularBuffer::get_spikes(Range range, unsigned long int* buf_out) return cnt_sp; } -Range CircularBuffer::get_value_range(Range range, unsigned int chk_step) +ValueRange CircularBuffer::get_value_range(IndexRange range, unsigned int chk_step) { using std::numeric_limits; - if (this->cnt == 0) return Range(0, 0); + if (this->cnt == 0) return ValueRange(0, 0); // indexes used during the calculation are "absolute" range = this->range().cut_range(range); @@ -290,13 +281,7 @@ Range CircularBuffer::get_value_range(Range range, unsigned int chk_step) return last.range_min_max; } -inline unsigned int div_ceil(unsigned int dividend, unsigned int divisor) -{ - unsigned int quo = dividend / divisor, rem = dividend % divisor; - if (rem > 0) quo++; return quo; -} - -float CircularBuffer::get_average(Range range, unsigned int chk_step) +float CircularBuffer::get_average(IndexRange range, unsigned int chk_step) { if (this->cnt == 0) return 0; @@ -330,7 +315,7 @@ float CircularBuffer::get_average(Range range, unsigned int chk_step) il_sub = last.range_i_av_val.min(); ir_sub = range.min() - 1; cnt_operate += ir_sub - il_sub + 1; } - flag_optimize = (cnt_operate < range.length()); + flag_optimize = (cnt_operate < range.count()); } if (! flag_optimize) { @@ -343,19 +328,19 @@ float CircularBuffer::get_average(Range range, unsigned int chk_step) } if (flag_optimize) { - cnt = div_ceil(last.range_i_av_val.length() + 1, chk_step); + cnt = last.range_i_av_val.count_by_step(chk_step); sum = cnt * last.av_val; } float* p_add, * p_sub, * p_add_end, * p_sub_end; if (flag_add) { - unsigned int add_cnt = div_ceil(ir_add - il_add + 1, chk_step); + unsigned int add_cnt = IndexRange(il_add, ir_add).count_by_step(chk_step); p_add = this->item_addr(this->index_to_rel(il_add)); p_add_end = this->ptr_inc(p_add, (add_cnt - 1)*chk_step); cnt += add_cnt; } if (flag_subtract) { - unsigned int sub_cnt = div_ceil(ir_sub - il_sub + 1, chk_step); + unsigned int sub_cnt = IndexRange(il_sub, ir_sub).count_by_step(chk_step); p_sub = this->item_addr(this->index_to_rel(il_sub)); p_sub_end = this->ptr_inc(p_sub, (sub_cnt - 1)*chk_step); cnt -= sub_cnt; diff --git a/circularbuffer.h b/circularbuffer.h index a4cd1c1..d67554e 100644 --- a/circularbuffer.h +++ b/circularbuffer.h @@ -21,45 +21,17 @@ namespace SimpleCairoPlot { +class CircularBuffer; struct BufRangeMap; + +// mapping from index range in the circular buffer to 1 or 2 segment(s) in memory +struct BufRangeMap { + IndexRange former, latter; + BufRangeMap(); + BufRangeMap(IndexRange range, unsigned int bufsize, unsigned int cur); +}; class CircularBuffer { - float* buf = NULL; float* bufend = NULL; - float* end = NULL; //points to where the next item should be stored in - unsigned int bufsize = 0, cnt = 0; - volatile unsigned long int cnt_overwrite = 0; - - // used for spike check - float spike_check_ref_min = 0; - unsigned long int* buf_spike = NULL, * buf_spike_bufend = NULL; - unsigned long int* buf_spike_end = NULL; - unsigned int buf_spike_size = 0, buf_spike_cnt = 0; - float spike_check_av = 0; - - // used for optimization (indexes are "absolute") - struct MinMaxScanInfo { - Range range_i_min_max_scan = Range(-1, -1), - range_i_min_max = Range(0, 0), //two indexes stored as a range for convenience - range_min_max = Range(0, 0); - }; - struct AvCalcInfo { - Range range_i_av_val = Range(-1, -1); - float av_val = 0; - }; - std::atomic last_min_max_scan; - std::atomic last_av_calc; - - // used to avoid multithreaded conflicts - std::atomic_flag flag_lock = ATOMIC_FLAG_INIT; //atomic_flag is not implemented with mutex - std::atomic_int read_lock_counter; //atomic_int is not implemented with mutex on most platforms - - void copy_from(const CircularBuffer& from); - float* ptr_inc(float* p, unsigned int inc = 1) const; - float* item_addr(unsigned int i) const; - unsigned long int buf_spike_item(unsigned int i) const; - void buf_spike_push(unsigned long int val); - void spike_check(); - public: // locks for writing (except the constructor without parameter and the destructor) CircularBuffer(); void init(unsigned int sz); //init() must be called if this constructor is used @@ -70,18 +42,18 @@ class CircularBuffer ~CircularBuffer(); unsigned int size() const; unsigned int spike_buffer_size() const; - bool is_valid_range(Range range) const; + bool is_valid_range(IndexRange range) const; unsigned int count() const; - Range range() const; - Range range_max() const; + IndexRange range() const; + IndexRange range_max() const; bool is_full() const; unsigned long int count_overwritten() const; unsigned long int count_overall() const; unsigned long int index_to_abs(unsigned int i) const; //returns a fixed index after filling unsigned int index_to_rel(unsigned long int i) const; //turn back to "relative" index - Range range_to_abs(Range range) const; - Range range_to_rel(Range range_abs) const; + IndexRange range_to_abs(IndexRange range) const; + IndexRange range_to_rel(IndexRange range_abs) const; float& item(unsigned int i) const; float& operator[](unsigned int i) const; @@ -97,22 +69,78 @@ class CircularBuffer // get_spikes() locks for reading void set_spike_check_ref_min(float val); unsigned int get_spikes(unsigned int* buf_out); - unsigned int get_spikes(Range range, unsigned int* buf_out); - unsigned int get_spikes(Range range, unsigned long int* buf_out); + unsigned int get_spikes(IndexRange range, unsigned int* buf_out); + unsigned int get_spikes(IndexRange range, unsigned long int* buf_out); // locks for reading; optimized for scrolling right - Range get_value_range(unsigned int chk_step = 1); - Range get_value_range(Range range, unsigned int chk_step = 1); + ValueRange get_value_range(unsigned int chk_step = 1); + ValueRange get_value_range(IndexRange range, unsigned int chk_step = 1); float get_average(unsigned int chk_step = 1); - float get_average(Range range, unsigned int chk_step = 1); + float get_average(IndexRange range, unsigned int chk_step = 1); // the buffer can be locked externally ONLY before writing to or reading multiple // data from the buffer through operator[]; member functions that lock for writing // should NOT be called inside that lock() and unlock() pair. void lock(bool for_writing = false); void unlock(); + +private: + float* buf = NULL; float* bufend = NULL; + float* end = NULL; //points to where the next item should be stored in + unsigned int bufsize = 0, cnt = 0; + volatile unsigned long int cnt_overwrite = 0; + + // used for spike check + float spike_check_ref_min = 0; + unsigned long int* buf_spike = NULL, * buf_spike_bufend = NULL; + unsigned long int* buf_spike_end = NULL; + unsigned int buf_spike_size = 0, buf_spike_cnt = 0; + float spike_check_av = 0; + + // used for optimization (indexes are "absolute") + struct MinMaxScanInfo { + IndexRange range_i_min_max_scan, + range_i_min_max; //two indexes stored as a range for convenience + ValueRange range_min_max = ValueRange(0, 0); + }; + struct AvCalcInfo { + IndexRange range_i_av_val; + float av_val = 0; + }; + std::atomic last_min_max_scan; + std::atomic last_av_calc; + + // used to avoid multithreaded conflicts + std::atomic_flag flag_lock = ATOMIC_FLAG_INIT; //atomic_flag is not implemented with mutex + std::atomic_int read_lock_counter; //atomic_int is not implemented with mutex on most platforms + + void copy_from(const CircularBuffer& from); + float* ptr_inc(float* p, unsigned int inc = 1) const; + float* item_addr(unsigned int i) const; + BufRangeMap map_from(IndexRange range) const; + unsigned long int buf_spike_item(unsigned int i) const; + void buf_spike_push(unsigned long int val); + void spike_check(); }; +inline BufRangeMap::BufRangeMap() {} + +inline BufRangeMap::BufRangeMap(IndexRange range, unsigned int bufsize, unsigned int cur) +{ + if (bufsize < 0 || cur >= bufsize) return; + range.fit_by_range(IndexRange(0, bufsize - 1)); if (!range) return; + + unsigned int il = cur + range.min(); + if (il >= bufsize) il -= bufsize; + + unsigned int ir = il + range.count() - 1; + if (ir >= bufsize) { + this->former.set(il, bufsize - 1); + this->latter.set(0, ir - bufsize); + } else + this->former.set(il, ir); +} + inline unsigned int CircularBuffer::size() const { return this->bufsize; @@ -123,9 +151,9 @@ inline unsigned int CircularBuffer::spike_buffer_size() const return this->buf_spike_size; } -inline bool CircularBuffer::is_valid_range(Range range) const +inline bool CircularBuffer::is_valid_range(IndexRange range) const { - return range.min() >= 0 && range.max() < this->bufsize; + return range && range.max() < this->bufsize; } inline unsigned int CircularBuffer::count() const @@ -133,17 +161,17 @@ inline unsigned int CircularBuffer::count() const return this->cnt; } -inline Range CircularBuffer::range() const +inline IndexRange CircularBuffer::range() const { if (this->cnt > 0) - return Range(0, this->cnt - 1); + return IndexRange(0, this->cnt - 1); else - return Range(0, 0); + return IndexRange(); } -inline Range CircularBuffer::range_max() const +inline IndexRange CircularBuffer::range_max() const { - return Range(0, this->bufsize - 1); + return IndexRange(0, this->bufsize - 1); } inline bool CircularBuffer::is_full() const @@ -175,16 +203,16 @@ inline unsigned int CircularBuffer::index_to_rel(unsigned long int i) const return 0; } -inline Range CircularBuffer::range_to_abs(Range range) const +inline IndexRange CircularBuffer::range_to_abs(IndexRange range) const { - Range range_abs = range; + IndexRange range_abs = range; range_abs.move(this->cnt_overwrite); return range_abs; } -inline Range CircularBuffer::range_to_rel(Range range_abs) const +inline IndexRange CircularBuffer::range_to_rel(IndexRange range_abs) const { - Range range = range_abs; + IndexRange range = range_abs; range.min_move_to(this->index_to_rel(range_abs.min())); return range; } @@ -242,7 +270,7 @@ inline unsigned int CircularBuffer::get_spikes(unsigned int* buf_out) return this->get_spikes(this->range(), buf_out); } -inline Range CircularBuffer::get_value_range(unsigned int chk_step) +inline ValueRange CircularBuffer::get_value_range(unsigned int chk_step) { return this->get_value_range(this->range(), chk_step); } @@ -273,18 +301,15 @@ inline void CircularBuffer::lock(bool for_writing) inline void CircularBuffer::unlock() { - // in case of two threads trying to unlock at the same time when the - // counter's original value is 1, because read_lock_counter is atomic, - // at least one of the two threads can fix the minus value problem. if (this->read_lock_counter.load(std::memory_order_acquire) > 0) { int counter = --this->read_lock_counter; if (counter > 0) return; - if (counter < 0) this->read_lock_counter = 0; + if (counter < 0) this->read_lock_counter = 0; //no effect if lock/unlock are paired } this->flag_lock.clear(std::memory_order_release); } -// private +/*------------------------------ private functions ------------------------------*/ inline float* CircularBuffer::ptr_inc(float* p, unsigned int inc) const { @@ -302,6 +327,11 @@ inline float* CircularBuffer::item_addr(unsigned int i) const return this->ptr_inc(this->end, i); } +inline BufRangeMap CircularBuffer::map_from(IndexRange range) const +{ + return BufRangeMap(range, this->bufsize, this->item_addr(0) - this->buf); +} + inline unsigned long int CircularBuffer::buf_spike_item(unsigned int i) const { unsigned long int* p; diff --git a/demo.cpp b/demo.cpp index df3403d..c67bcd9 100644 --- a/demo.cpp +++ b/demo.cpp @@ -12,7 +12,7 @@ using namespace SimpleCairoPlot; class Demo { Frontend frontend; - unsigned int freq = 2; //Hz + unsigned int freq = 2; //Hz, sine wave frequency steady_clock::time_point t_start; float t(); @@ -40,17 +40,10 @@ Demo::Demo() void Demo::run() { - // the recorder is created after the frontend opens, but on Windows - // the frontend can only be opened by `Frontend::run()`, which blocks - // current thread immediately. If you need to change options of the recorder, - // you must have another thread to do it. - this->t_start = steady_clock::now(); -#ifndef _WIN32 this->frontend.open(); this->frontend.recorder().set_interval(10); -#endif this->frontend.run(); //blocks } diff --git a/frontend.cpp b/frontend.cpp index 24b2ca7..58dd39d 100644 --- a/frontend.cpp +++ b/frontend.cpp @@ -9,6 +9,9 @@ #include +using namespace std::chrono; +using namespace std::this_thread; + using namespace SimpleCairoPlot; Frontend::Frontend() {} @@ -18,11 +21,6 @@ Frontend::Frontend(std::vector& ptrs, unsigned int buf_size) this->init(ptrs, buf_size); } -Frontend::~Frontend() -{ - this->close(); -} - void Frontend::init(std::vector& ptrs, unsigned int buf_size) { if (ptrs.size() == 0 || buf_size < 2) @@ -32,26 +30,14 @@ void Frontend::init(std::vector& ptrs, unsigned int buf_size) } #ifndef _WIN32 + void Frontend::open() { if (this->thread_gtk || this->window) return; this->thread_gtk = new std::thread(&Frontend::app_run, this); - while (! this->window) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); -} -#endif - -Recorder& Frontend::recorder() const -{ - unsigned int wait_ms = 0; - while (!this->window && wait_ms < 5000) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); wait_ms += 10; - } - if (! this->window) - throw std::runtime_error("Frontend::recorder(): frontend is not opened."); - return *this->rec; + while (! this->window) sleep_for(milliseconds(10)); + sleep_for(milliseconds(10)); } void Frontend::run() @@ -60,40 +46,121 @@ void Frontend::run() this->thread_gtk->join(); delete this->thread_gtk; this->thread_gtk = NULL; } else + this->app_run(); //run on current thread +} + +void Frontend::close() +{ + if (! this->window) return; + + this->rec->stop(); + this->dispatcher_quit->emit(); //eventually deletes the dispatcher itself + + if (this->thread_gtk) + this->run(); + else //this function is called from another thread too + while (this->window) sleep_for(milliseconds(1)); +} + +Frontend::~Frontend() +{ + this->close(); +} + +#else + +void Frontend::open() +{ + if (this->thread_gtk || this->window) return; + + if (! thread_gtk) + thread_gtk = new std::thread(&Frontend::thread_loop, this); + else + this->flag_open = true; + while (! this->window) sleep_for(milliseconds(10)); +} + +void Frontend::run() +{ + if (! this->thread_gtk) this->app_run(); + else { + this->flag_open = true; + while (this->flag_open) + sleep_for(milliseconds(500)); + } } void Frontend::close() { if (! this->window) return; - if (this->rec) this->rec->stop(); - this->dispatcher_gtk->connect(sigc::mem_fun(*this, &Frontend::close_window)); - this->dispatcher_gtk->emit(); //eventually deletes itself + this->rec->stop(); + this->dispatcher_quit->emit(); //eventually deletes itself - if (this->thread_gtk) { - this->thread_gtk->join(); - delete this->thread_gtk; this->thread_gtk = NULL; - } else { - while (this->window) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + while (this->window) sleep_for(milliseconds(10)); +} + +Frontend::~Frontend() +{ + if (! thread_gtk) return; + + if (this->window) + this->dispatcher_quit->emit(); + this->flag_destruct = true; + this->thread_gtk->detach(); + delete this->thread_gtk; +} + +#endif + +Recorder& Frontend::recorder() const +{ + unsigned int wait_ms = 0; + while (!this->window && wait_ms < 5000) { + sleep_for(milliseconds(10)); wait_ms += 10; } + if (! this->window) + throw std::runtime_error("Frontend::recorder(): frontend is not opened."); + return *this->rec; } /*------------------------------ private functions ------------------------------*/ +#ifdef _WIN32 +void Frontend::thread_loop() +{ + while (! flag_destruct) { + if (! flag_open) { + sleep_for(milliseconds(200)); continue; + } + this->app_run(); + flag_open = false; + } + + while (true) //to avoid segment fault, keep this thread until the real main thread exits + sleep_for(seconds(10)); +} +#endif + void Frontend::app_run() { - Glib::RefPtr app = Gtk::Application::create(this->app_name); - this->dispatcher_gtk = new Glib::Dispatcher; + std::string app_name = "org.simple-cairo-plot.frontend_"; + app_name += std::to_string(steady_clock::now().time_since_epoch().count()); + Glib::RefPtr app = Gtk::Application::create(app_name); + + this->dispatcher_quit = new Glib::Dispatcher; + this->dispatcher_quit->connect(sigc::mem_fun(*(app.get()), &Gtk::Application::quit)); + this->create_window(); this->create_file_dialog(); app->run(*this->window); this->window = NULL; //the window is already destructed when the thread exits Application::run() - delete this->file_dialog; delete this->dispatcher_gtk; -} // Unsolved problem on Windows when running in a new thread created by open(): Segmentation fault received here. + delete this->file_dialog; + delete this->dispatcher_quit; +} void Frontend::create_window() { @@ -108,7 +175,7 @@ void Frontend::create_window() * button_save = Gtk::manage(new Gtk::Button("Save")); button_open->signal_clicked().connect(sigc::mem_fun(*this, &Frontend::on_button_open_clicked)); button_save->signal_clicked().connect(sigc::mem_fun(*this, &Frontend::on_button_save_clicked)); - + Gtk::Box* box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)), * bar = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); @@ -133,6 +200,7 @@ void Frontend::create_file_dialog() Glib::RefPtr filter = Gtk::FileFilter::create(); filter->set_name("CSV files (.csv)"); filter->add_pattern("*.csv"); filter->add_mime_type("text/csv"); + this->file_dialog = new Gtk::FileChooserDialog("Open .csv file", Gtk::FILE_CHOOSER_ACTION_OPEN); this->file_dialog->set_select_multiple(false); this->file_dialog->add_filter(filter); @@ -200,9 +268,3 @@ void Frontend::on_button_save_clicked() if (! suc) this->window->set_title(this->title + " - Failed to save as file"); } -// used by dispatcher_gtk -void Frontend::close_window() -{ - this->window->close(); -} - diff --git a/frontend.h b/frontend.h index a869375..ea771c0 100644 --- a/frontend.h +++ b/frontend.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace SimpleCairoPlot { @@ -20,12 +21,17 @@ class Frontend: public sigc::trackable Recorder* rec; std::thread* thread_gtk = NULL; - Glib::Dispatcher* dispatcher_gtk = NULL; + Glib::Dispatcher* dispatcher_quit = NULL; + Gtk::Window* volatile window = NULL; - Gtk::Window* window = NULL; Gtk::FileChooserDialog* file_dialog; Gtk::Button* button_start_stop; +#ifdef _WIN32 + volatile bool flag_open = true, flag_destruct = false; + void thread_loop(); +#endif + void create_window(); void create_file_dialog(); void app_run(); @@ -35,11 +41,7 @@ class Frontend: public sigc::trackable void on_button_open_clicked(); void on_button_save_clicked(); - void close_window(); - public: - // two frontends on the same computer must have different `app_name`. - std::string app_name = "org.simple-cairo-plot.frontend"; std::string title = "Recorder"; Frontend(); void init(std::vector& ptrs, unsigned int buf_size); @@ -48,10 +50,8 @@ class Frontend: public sigc::trackable Frontend& operator=(const Frontend&) = delete; virtual ~Frontend(); -#ifndef _WIN32 void open(); //create a new thread to run the frontend -#endif - Recorder& recorder() const; //notice: don't keep the returned reference when you need to close the frontend + Recorder& recorder() const; //notice: don't keep the returned reference when closing the frontend void run(); //run in current thread or join the existing frontend thread, blocks void close(); }; diff --git a/plotarea.cpp b/plotarea.cpp new file mode 100644 index 0000000..38d24d1 --- /dev/null +++ b/plotarea.cpp @@ -0,0 +1,760 @@ +// by wuwbobo2021 , +// If you have found bugs in this program, please pull an issue, or contact me. +// Licensed under LGPL version 2.1. + +#include + +#include +#include +#include + +using namespace SimpleCairoPlot; + +PlotArea::PlotArea() {} + +PlotArea::PlotArea(CircularBuffer* buf) +{ + this->init(buf); +} + +void PlotArea::init(CircularBuffer* buf) +{ + if (this->flag_auto_refresh) this->set_refresh_mode(false); + + if (! buf) + throw std::invalid_argument("PlotArea::init(): the buffer pointer is null."); + this->source = buf; + + unsigned int limit_max = 2 * this->get_screen()->get_monitor_workarea().get_width(); + if (limit_max > this->source->size()) limit_max = this->source->size(); + this->plot_data_amount_max_range.set(Plot_Data_Amount_Limit_Min, limit_max); + + this->buf_plot.init(buf, limit_max); + + this->signal_size_allocate().connect(sigc::mem_fun(*this, &PlotArea::on_size_allocation)); + this->dispatcher.connect(sigc::bind(sigc::mem_fun(*this, &PlotArea::draw), + (Cairo::RefPtr)nullptr)); + + this->param.color_plot.set_rgba(1.0, 0.0, 0.0); //red + this->oss.setf(std::ios::fixed); +} + +PlotArea::~PlotArea() +{ + this->set_refresh_mode(false); //make sure the thread is ended +} + +bool PlotArea::set_refresh_mode(bool auto_refresh, unsigned int interval) +{ + if (this->source == NULL && auto_refresh) + throw std::runtime_error("PlotArea::set_refresh_mode(): pointer of source data buffer is not set."); + + if (interval > 0) { + if (interval < 40) interval = 40; //maximum graph refresh rate: 25 Hz + this->refresh_interval = interval; + } + + if (auto_refresh == this->flag_auto_refresh) return true; + this->flag_auto_refresh = auto_refresh; + if (auto_refresh) { + try { + this->thread_timer = new std::thread(&PlotArea::refresh_loop, this); + return true; + } catch (std::exception ex) { + this->flag_auto_refresh = false; + this->thread_timer = NULL; + return false; + } + } else { + if (! this->thread_timer) return true; + this->thread_timer->join(); + delete(this->thread_timer); + return true; + } +} + +void PlotArea::refresh(bool forced_check_range_y, bool forced_adapt, bool forced_sync) +{ + if (! this->source) + throw std::runtime_error("PlotArea::refresh(): not initialized."); + + if (forced_sync) this->flag_sync = true; + if (! this->get_visible()) { + if (option_auto_set_range_y) + this->flag_adapt = this->flag_check_range_y = true; + return; + } + if (flag_drawing) return; + + if (this->option_auto_goto_end) { + if (this->option_auto_extend_range_x) + this->range_x_extend(); + else + this->range_x_goto_end(); + } + + if (this->option_auto_set_range_y) { + if (forced_check_range_y) this->flag_check_range_y = true; + else if (++this->counter1 > 5) { + this->flag_check_range_y = true; this->counter1 = 0; + } + } + if (this->flag_check_range_y) { + if (forced_adapt) this->flag_adapt = true; + else if (++this->counter2 > 5) { + this->flag_adapt = true; this->counter2 = 0; + } + } + + this->dispatcher.emit(); //let the main thread draw the frame +} + +bool PlotArea::set_range_x(IndexRange range) +{ + if (!range || range.count() < 2) return false; + if (! this->source->is_valid_range(range)) return false; + + this->range_x = range; + this->adjust_index_step(); + return true; +} + +bool PlotArea::set_range_y_length_min(float length_min) +{ + if (length_min < 0) return false; + this->range_y_length_min = length_min; + return true; +} + +void PlotArea::set_option_auto_goto_end(bool set) +{ + this->option_auto_goto_end = set; +} +void PlotArea::set_option_auto_extend_range_x(bool set) +{ + this->option_auto_extend_range_x = set; +} +void PlotArea::set_option_auto_set_range_y(bool set) +{ + this->option_auto_set_range_y = set; + if (! set) this->flag_check_range_y = false; +} +void PlotArea::set_option_auto_set_zero_bottom(bool set) +{ + this->option_auto_set_zero_bottom = set; +} + +void PlotArea::range_x_goto_end() +{ + if (this->source->count() >= this->range_x.count()) + this->range_x.max_move_to(this->source->count() - 1); + else + this->range_x.min_move_to(0); +} + +void PlotArea::range_x_extend(bool remain_space) +{ + if (this->range_x.contain(this->source->range())) return; + + if (remain_space) { + this->range_x.min_move_to(0); + if (this->range_x.contain(this->source->range())) return; + this->range_x.set(0, 2*this->range_x.count() - 1); + if (this->range_x.contain(this->source->range_max())) + this->range_x = this->source->range_max(); + else + this->range_x.fit_by_range(this->source->range_max()); + } else + this->range_x = this->source->range(); +} + +bool PlotArea::set_range_y(ValueRange range) +{ + if (range.length() == 0) return false; + if (! this->option_auto_set_range_y) { + this->param.range_y = range; return true; + } else return false; +} + +void PlotArea::range_y_auto_set(bool adapt) +{ + if (this->source->count() <= 1) { + this->param.range_y.set(0, 10); return; + } + + ValueRange range_tight = this->source->get_value_range(this->range_x, this->param.index_step); + if (adapt == false && this->param.range_y.contain(range_tight)) return; + + float min = range_tight.min(), max = range_tight.max(); + + if (min < 0 || this->option_auto_set_zero_bottom == false) { + if (max > min) { + this->param.range_y.set(min, max); this->param.range_y.scale(1.2); + } else + this->param.range_y.set(min - 0.2*min, min + 0.2*min); //max = min < 0, rare + + if (this->param.range_y.length() < this->range_y_length_min) + this->param.range_y.scale(this->range_y_length_min / this->param.range_y.length()); + + if (min >= 0 && this->param.range_y.min() < 0) + this->param.range_y.min_move_to(0); + } else { + // without any minus value, always set lower bound to 0 + if (max > 0) + this->param.range_y.set(0, 1.2*max); + else + this->param.range_y.set(0, 10); //min = max = 0, rare + + if (this->param.range_y.length() < this->range_y_length_min) + this->param.range_y.scale(this->range_y_length_min / this->param.range_y.length(), 0); + } + + this->source->set_spike_check_ref_min(range_tight.center()); +} + +bool PlotArea::set_axis_divider(unsigned int x_div, unsigned int y_div) +{ + if (x_div == 0 && y_div == 0) return false; + if (x_div == 0) x_div = 1; if (y_div == 0) y_div = 1; + + this->param.axis_x_divider = x_div; + this->param.axis_y_divider = y_div; + return true; +} + +bool PlotArea::set_axis_x_unit(float unit) +{ + if (unit <= 0) return false; + this->param.axis_x_unit = unit; + return true; +} + +void PlotArea::set_axis_x_unit_name(std::string str_unit) +{ + this->param.axis_x_unit_name = str_unit; +} + +void PlotArea::set_axis_y_unit_name(std::string str_unit) +{ + this->param.axis_y_unit_name = str_unit; +} + +void PlotArea::set_option_fixed_scale(bool set) +{ + this->param.option_fixed_scale = set; +} + +void PlotArea::set_option_show_axis_x_values(bool set) +{ + this->param.option_show_axis_x_values = set; +} + +void PlotArea::set_option_axis_x_int_values(bool set) +{ + this->param.option_axis_x_int_values = set; +} + +void PlotArea::set_option_show_axis_y_values(bool set) +{ + this->param.option_show_axis_y_values = set; +} + +void PlotArea::set_option_show_average_line(bool set) +{ + this->param.option_show_average_line = set; +} + +void PlotArea::set_plot_color(Gdk::RGBA color) +{ + this->param.color_plot = color; +} + +void PlotArea::set_option_anti_alias(bool set) +{ + this->param.option_anti_alias = set; +} + +/*------------------------------ private functions ------------------------------*/ + +void PlotArea::on_style_updated() +{ + if (! flag_set_colors) return; + + Gdk::RGBA color_fore = this->get_style_context()->get_color(); + this->color_text = color_fore; + + if ((color_fore.get_red() + color_fore.get_green() + color_fore.get_blue()) / 3 < 0.5) { //light background + this->color_back.set_rgba(1.0, 1.0, 1.0); //white + this->color_grid.set_rgba(0.8, 0.8, 0.8); //light gray + } else { + this->color_back.set_rgba(0.1, 0.1, 0.1); //black + this->color_grid.set_rgba(0.4, 0.4, 0.4); //deep gray + } + flag_set_colors = false; +} + +void PlotArea::on_size_allocation(Gtk::Allocation& allocation) +{ + // param.alloc is the area for plotting; alloc_outer might contain tick values. + this->param.alloc_outer = Gtk::Allocation(0, 0, allocation.get_width(), allocation.get_height()); + unsigned int border_x_left = (this->param.option_show_axis_y_values? this->Border_X_Left : 0); + this->param.alloc = Gtk::Allocation(border_x_left, this->Border_Y, + allocation.get_width() - border_x_left, + allocation.get_height() - 2*this->Border_Y); + this->adjust_index_step(); +} + +void PlotArea::adjust_index_step() +{ + unsigned int plot_data_amount_max = + this->plot_data_amount_max_range.fit_value(2 * this->param.alloc.get_width()); + + this->param.index_step = 1; + while (ceil(this->range_x.count() / this->param.index_step) > plot_data_amount_max) + this->param.index_step++; +} + +bool PlotArea::on_draw(const Cairo::RefPtr& cr) +{ + this->draw(cr); + return true; +} + +void PlotArea::refresh_loop() //in the timer thread +{ + using namespace std::chrono; + using namespace std::this_thread; + + while (this->flag_auto_refresh) { + steady_clock::time_point time_bef_draw = steady_clock::now(); + this->refresh(); + sleep_until(time_bef_draw + milliseconds(this->refresh_interval)); + } +} + +inline void set_cr_color(const Cairo::RefPtr& cr, const Gdk::RGBA& color) +{ + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); +} + +void PlotArea::draw(Cairo::RefPtr cr) +{ + Gtk::Allocation alloc = this->param.alloc_outer; + if (alloc.get_width() < 10 || alloc.get_height() < 10) return; + + this->flag_drawing = true; + + // refresh PlotParam + this->param.data_cnt = this->source->count(); + this->param.data_cnt_overall = this->source->count_overall(); + this->param.range_x = this->source->range_to_abs(this->range_x); + if (this->flag_check_range_y) { //this flag can be set by refresh() + this->range_y_auto_set(this->flag_adapt); + this->flag_adapt = this->flag_check_range_y = false; + } + if (this->param.option_show_average_line) { + float av = this->source->get_average(this->range_x, this->param.index_step); + this->param.y_av_alloc = this->param.range_y.map_reverse(av, this->param.alloc_y()); + } + + bool flag_clean = (bool)cr; //if cr is valid, it's passed from on_draw() + bool flag_redraw = ( flag_clean || this->flag_sync + || !this->param.reuse_graph(this->buf_plot.get_param())); + + Glib::RefPtr drawing_context; + if (!flag_clean) { + // create cairo context (optimized). the frame isn't double-buffered because this + // is not a top-level Gdk::Window (see reference of Gdk::Window::begin_draw_frame()). + cairo_rectangle_int_t rect = {0, 0, (int)alloc.get_width(), (int)alloc.get_height()}; + drawing_context = this->get_window()->begin_draw_frame(Cairo::Region::create(rect)); + cr = drawing_context->get_cairo_context(); + } else { //set background color (the widget's default background-color isn't known...) + set_cr_color(cr, this->color_back); cr->paint(); + } + + if (!flag_clean && flag_redraw) { + // do erasing instead of filling with back color to reduce CPU usage + set_cr_color(cr, this->color_back); cr->set_antialias(Cairo::ANTIALIAS_NONE); + cr->set_line_width(this->buf_plot.get_param().option_anti_alias? 2.0 : 1.0); + this->buf_plot.cairo_load(cr, true); cr->stroke(); + this->draw_grid(cr, this->buf_plot.get_param(), false); + } + + if (flag_redraw) + this->draw_grid(cr, this->param); + + set_cr_color(cr, this->param.color_plot); cr->set_line_width(1.0); + cr->set_antialias(this->param.option_anti_alias? Cairo::ANTIALIAS_GRAY : Cairo::ANTIALIAS_NONE); + this->buf_plot.sync(this->param, this->flag_sync); + this->buf_plot.cairo_load(cr, flag_redraw); cr->stroke(); + + if (! flag_clean) this->get_window()->end_draw_frame(drawing_context); + this->flag_sync = false; + this->flag_drawing = false; +} + +inline unsigned int get_precision(float len_seg) +{ + if (len_seg == 0) return 0; + float len = len_seg / 10.0; unsigned int i; + for (i = 0; len < 1; len *= 10.0, i++); + return i; +} + +inline std::string float_to_str(float val, std::ostringstream& oss) +{ + oss.str(""); oss << val; + return oss.str(); +} + +void PlotArea::draw_grid(Cairo::RefPtr cr, const PlotParam& param, bool not_erase) +{ + float inner_x1 = param.alloc.get_x(), + inner_y1 = param.alloc.get_y(); + float inner_x2 = inner_x1 + param.alloc.get_width(), + inner_y2 = inner_y1 + param.alloc.get_height(); + + AxisRange alloc_x(inner_x1, inner_x2), + alloc_y(inner_y1, inner_y2); + + AxisRange range_val_x = param.range_x; + range_val_x.scale(param.axis_x_unit, 0); + + AxisValues axis_x_values(range_val_x, param.axis_x_divider, !param.option_fixed_scale), + axis_y_values(param.range_y, param.axis_y_divider, !param.option_fixed_scale); + + set_cr_color(cr, not_erase? this->color_grid : this->color_back); + cr->set_antialias(Cairo::ANTIALIAS_NONE); + + // draw border + cr->set_line_width(2.0); + cr->rectangle(inner_x1 + 1.0, inner_y1 + 1.0, + inner_x2 - inner_x1 - 3.0, inner_y2 - inner_y1 - 3.0); + cr->stroke(); + + // draw grid + float x, y; cr->set_line_width(1.0); + for (unsigned int i = 0; i < axis_x_values.count(); i++) { + x = range_val_x.map(axis_x_values[i], alloc_x); + cr->move_to(x, inner_y1); + cr->line_to(x, inner_y2); + } + for (unsigned int i = 0; i < axis_y_values.count(); i++) { + y = param.range_y.map_reverse(axis_y_values[i], alloc_y); + cr->move_to(inner_x1, y); + cr->line_to(inner_x2, y); + } + cr->stroke(); + + if (param.option_show_average_line) { + y = param.y_av_alloc; + if (not_erase) { + set_cr_color(cr, this->color_text); + cr->set_dash(this->dash_pattern, 0); + } + cr->move_to(inner_x1, y); + cr->line_to(inner_x2, y); + cr->stroke(); cr->unset_dash(); + } + + // print value labels for axis x, y + + if (not_erase && (param.option_show_axis_x_values || param.option_show_axis_y_values)) { + oss.clear(); cr->set_font_size(12); set_cr_color(cr, this->color_text); + } + + if (param.option_show_axis_x_values) { + if (not_erase) { + if (param.option_axis_x_int_values) + oss.precision(0); + else + oss.precision(get_precision(range_val_x.length() / param.axis_x_divider)); + + float val; std::string str_x_val, str_x_val_prev = ""; + for (unsigned int i = 0; i < axis_x_values.count(); i++) { + val = axis_x_values[i]; + x = range_val_x.map(val, alloc_x); + if (inner_x2 - x < 50) break; + str_x_val = float_to_str(val, this->oss); + if (!param.option_axis_x_int_values || str_x_val != str_x_val_prev) { + cr->move_to(x, inner_y2 + 12); + cr->show_text(str_x_val); + } + if (param.option_axis_x_int_values) str_x_val_prev = str_x_val; + } + + // show axis x unit name + if (param.axis_x_unit_name.length() > 0) { + cr->move_to(inner_x2 - (param.axis_x_unit_name.length() + 2) * 5, inner_y2 + 12); + cr->show_text('(' + param.axis_x_unit_name + ')'); + } + } else { + cr->rectangle(inner_x1, inner_y2, alloc_x.length(), Border_Y); cr->fill(); + } + } + + if (param.option_show_axis_y_values) { + float outer_x1 = param.alloc_outer.get_x(); + if (not_erase) + oss.precision(get_precision(param.range_y.length() / param.axis_y_divider)); + float val; + for (unsigned int i = 0; i < axis_y_values.count(); i++) { + val = axis_y_values[i]; + y = param.range_y.map_reverse(val, alloc_y); + + if (! not_erase) { + cr->rectangle(outer_x1, y - 12, Border_X_Left + 30, 14); + cr->fill(); continue; + } + cr->move_to(outer_x1, y); + if (i < axis_y_values.count() - 1 || param.axis_y_unit_name.length() == 0) + cr->show_text(float_to_str(val, this->oss)); + else { // print topmost value with axis y unit name added + y -= 2; cr->move_to(outer_x1, y); + cr->show_text(float_to_str(val, this->oss) + '(' + param.axis_y_unit_name + ')'); + } + } + } +} + +/*------------------------------ PlotParam functions ------------------------------*/ + +bool PlotParam::reuse_graph(const PlotParam& prev) const +{ + return this->data_cnt >= prev.data_cnt //buffer has not been cleared + && this->data_cnt_overall >= prev.data_cnt_overall + && this->alloc.get_width() == prev.alloc.get_width() + && this->alloc.get_height() == prev.alloc.get_height() + && this->y_av_alloc == prev.y_av_alloc + && this->range_x == prev.range_x + && this->range_y == prev.range_y + && this->index_step == prev.index_step + + && this->color_plot == prev.color_plot + && this->option_anti_alias == prev.option_anti_alias + && this->axis_x_divider == prev.axis_x_divider + && this->axis_y_divider == prev.axis_y_divider + + && this->option_fixed_scale == prev.option_fixed_scale + && this->option_show_axis_x_values == prev.option_show_axis_x_values + && this->option_show_axis_y_values == prev.option_show_axis_y_values + && this->option_show_average_line == prev.option_show_average_line + + && ( !this->option_show_axis_x_values + || ( this->option_axis_x_int_values == prev.option_axis_x_int_values + && this->axis_x_unit == prev.axis_x_unit + && this->axis_x_unit_name == prev.axis_x_unit_name)) + && ( !this->option_show_axis_y_values + || this->axis_y_unit_name == prev.axis_y_unit_name); +} + +bool PlotParam::reuse_data(const PlotParam& prev) const +{ + return this->data_cnt >= prev.data_cnt + && this->data_cnt_overall >= prev.data_cnt_overall + && this->index_step == prev.index_step + && this->range_y == prev.range_y + && this->alloc.get_height() == prev.alloc.get_height() + && intersection(this->range_x, prev.range_x).count() >= prev.index_step; +} + +/*------------------------------ PlotBuffer functions ------------------------------*/ + +PlotBuffer::PlotBuffer() {} + +PlotBuffer::PlotBuffer(CircularBuffer* src, unsigned int cnt_limit) +{ + this->init(src); +} + +void PlotBuffer::init(CircularBuffer* src, unsigned int cnt_limit) +{ + this->source = src; + this->buf_cr_cnt_max = cnt_limit; + + this->buf_cr_size = 2 * cnt_limit; + unsigned int buf_cr_spike_size = 4 * src->spike_buffer_size(); + bool except_caught = false; + try { + this->buf_spike = new unsigned long int[src->spike_buffer_size()]; + this->buf_cr = new cairo_path_data_t[buf_cr_size + buf_cr_spike_size]; + } catch (std::bad_alloc) { + except_caught = true; + } + if (except_caught || this->buf_spike == NULL || this->buf_cr == NULL) { + if (this->buf_spike) {delete this->buf_spike; this->buf_spike = NULL;} + throw std::bad_alloc(); + } + + this->buf_cr_spike = this->buf_cr + buf_cr_size; + + // initialize the cairo path buffer + cairo_path_data_t data_head; + data_head.header.type = CAIRO_PATH_LINE_TO; data_head.header.length = 2; + for (unsigned int i = 0; i < buf_cr_size; i += 2) + this->buf_cr[i] = data_head; + + // initialize the spike segment of the cairo path buffer + data_head.header.type = CAIRO_PATH_MOVE_TO; + for (unsigned int i = 0; i < buf_cr_spike_size; i += 4) + this->buf_cr_spike[i] = data_head; + data_head.header.type = CAIRO_PATH_LINE_TO; + for (unsigned int i = 2; i < buf_cr_spike_size; i += 4) + this->buf_cr_spike[i] = data_head; +} + +PlotBuffer::~PlotBuffer() +{ + if (this->buf_spike) delete this->buf_spike; + if (this->buf_cr) delete this->buf_cr; +} + +bool PlotBuffer::sync(const PlotParam& param, bool forced_sync) +{ + unsigned int step = param.index_step; + if (! param) return false; + if (param.range_x.count_by_step(step) > this->buf_cr_cnt_max) return false; + this->source->lock(); + + this->flag_redraw = forced_sync || !param.reuse_graph(this->param); + + IndexRange range_data = param.data_range_x(), + range_data_pre = this->param.data_range_x(); + range_data.step_align_with(range_data_pre, step); + + IndexRange range_data_l, range_data_r; unsigned int cur_buf_l, cur_buf_r; + bool flag_reuse_data = true; + + // calculate the ranges of new data to be loaded + if (this->flag_redraw || range_data.max() > range_data_pre.max()) { + if (param.alloc_x_step() != this->param.alloc_x_step()) + this->buf_cr_refresh_x(param.alloc_x_step()); + + // check if y-axis data can be reused + if (!forced_sync && param.reuse_data(this->param)) { + if (range_data.min() < range_data_pre.min()) { + range_data_l.set(range_data.min(), range_data_pre.min() - 1); + cur_buf_l = this->cur_move + (this->cur_buf_cr, -(long int)range_data_l.count_by_step(step)); + } + range_data_r.set(range_data_pre.max() + 1, range_data.max()); + if (range_data_r) cur_buf_r = this->cur_move(this->cur_buf_cr, this->cnt_buf_cr); + } else { + flag_reuse_data = false; + cur_buf_l = 0; range_data_l = range_data; + } + } + + this->param = param; + if (param.index_step > 1 && (range_data_l || range_data_r)) + this->buf_cr_spike_sync(); + this->source->unlock(); + + if (range_data_l) this->buf_cr_load(cur_buf_l, range_data_l); + if (range_data_r) this->buf_cr_load(cur_buf_r, range_data_r); + + if (flag_reuse_data) + this->cur_buf_cr = this->cur_move(this->cur_buf_cr, + subtract(range_data.min(), range_data_pre.min()) / (int)this->param.index_step); + else + this->cur_buf_cr = 0; + this->cnt_buf_cr = range_data.count_by_step(this->param.index_step); + + if (! this->flag_redraw) { + this->cur_ext = cur_buf_r; + this->cnt_ext = range_data_r.count_by_step(this->param.index_step); //0 if range_data_r is empty + } + + return true; +} + +void PlotBuffer::buf_cr_refresh_x(float x_step) +{ + if (x_step == 0 || x_step == this->buf_cr_x_step) return; + + float x = 0; + for (unsigned int i = 0, i_buf = 1; i < buf_cr_cnt_max; i++, i_buf += 2) { + this->buf_cr[i_buf].point.x = x; x += x_step; + } + this->buf_cr_x_step = x_step; +} + +void PlotBuffer::buf_cr_load(unsigned int cur_buf_cr, IndexRange range_data) +{ + AxisRange alloc_y = this->param.alloc_y(); + this->i_buf_cr = this->cur_to_i(cur_buf_cr); + for (unsigned int i = range_data.min(); i <= range_data.max(); i += this->param.index_step) + this->buf_cr_add(this->param.range_y.map_reverse(this->source->abs_index_item(i), alloc_y)); +} + +void PlotBuffer::buf_cr_spike_sync() +{ + unsigned int cnt_sp = this->source->get_spikes + (this->source->range_to_rel(this->param.range_x), this->buf_spike); + if (cnt_sp == 0) return; + + AxisRange alloc_x = this->param.alloc_x(), alloc_y = this->param.alloc_y(); + float x_step = alloc_x.length() / this->param.range_x.length(); + + unsigned long int i; float x, y; + this->i_buf_cr_spike = 1; + for (unsigned int i_sp = 0; i_sp < cnt_sp; i_sp++) { + i = this->buf_spike[i_sp]; + x = this->param.range_x.map(i, alloc_x); + y = this->param.range_y.map_reverse(this->source->abs_index_item(i), alloc_y); + this->buf_cr_spike_add(x, y); + x += x_step; + y = this->param.range_y.map_reverse(this->source->abs_index_item(i + 1), alloc_y); + this->buf_cr_spike_add(x, y); + } +} + +inline void cr_append(const Cairo::RefPtr& cr, cairo_path_data_t* data, int num_data) +{ + if (num_data == 0) return; + cairo_path_t path_info = {CAIRO_STATUS_SUCCESS, data, num_data}; + + bool flag_recover = false; + if (data->header.type == CAIRO_PATH_LINE_TO) { + data->header.type = CAIRO_PATH_MOVE_TO; + flag_recover = true; + } + + cairo_append_path(cr->cobj(), &path_info); + + if (flag_recover) + data->header.type = CAIRO_PATH_LINE_TO; +} + +void PlotBuffer::cairo_load(const Cairo::RefPtr& cr, bool forced_redraw) +{ + if (! this->cnt_buf_cr) return; + if (forced_redraw) this->flag_redraw = true; + if (!this->flag_redraw && !this->cnt_ext) return; + + unsigned int cur, cnt; float x_cur = this->param.alloc.get_x(); + if (this->flag_redraw) { + cur = this->cur_buf_cr; cnt = this->cnt_buf_cr; + } else { + cur = this->cur_move(this->cur_ext, -1); cnt = this->cnt_ext + 1; //start from the end of previous segment + x_cur += (this->cnt_buf_cr - cnt)*this->param.alloc_x_step(); + } + BufRangeMap map(IndexRange(0, cnt - 1), this->buf_cr_cnt_max, cur); + + Cairo::Matrix matrix_org = cr->get_matrix(); //this is useful if cr is provided by on_draw() + cr->translate(-(float)map.former.min()*this->param.alloc_x_step() + x_cur, 0); + cr_append(cr, this->buf_cr + this->cur_to_i(map.former.min()) - 1, 2*map.former.count()); + + if (map.latter) { + cr->set_matrix(matrix_org); + cr->translate(x_cur + map.former.count()*this->param.alloc_x_step(), 0); + cr_append(cr, this->buf_cr + this->cur_to_i(map.latter.min()) - 1, 2*map.latter.count()); + } + + cr->set_matrix(matrix_org); + if (this->param.index_step > 1) + cr_append(cr, this->buf_cr_spike, this->i_buf_cr_spike - 2 + 1); + + flag_redraw = false; +} + diff --git a/plotarea.h b/plotarea.h new file mode 100644 index 0000000..4aedc19 --- /dev/null +++ b/plotarea.h @@ -0,0 +1,275 @@ +// by wuwbobo2021 , +// If you have found bugs in this program, please pull an issue, or contact me. +// Licensed under LGPL version 2.1. + +#ifndef SIMPLE_CAIRO_PLOT_AREA_H +#define SIMPLE_CAIRO_PLOT_AREA_H + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace SimpleCairoPlot +{ +class PlotArea; +class PlotParam; class PlotBuffer; //owned by PlotArea + +using PlottingArea = PlotArea; //v1.0.x name + +// please skip to class PlotArea + +struct PlotParam +{ + // current conditions + unsigned int data_cnt = 0; unsigned long int data_cnt_overall = 0; + Gtk::Allocation alloc, alloc_outer; //topleft point of alloc_outer is always (0, 0) + unsigned int y_av_alloc = 0; //don't care if option_show_average_line is not set + + IndexRange range_x; //different from PlotArea::range_x, it's the "absolute" index range of plotting data + ValueRange range_y = ValueRange(0, 10); + unsigned int index_step = 1; //it will be adjusted when range_x is too wide + + // stored options + Gdk::RGBA color_plot; bool option_anti_alias = false; + + unsigned int axis_x_divider = 5, axis_y_divider = 6; + bool option_fixed_scale = true; + bool option_show_axis_x_values = true; bool option_axis_x_int_values = false; + bool option_show_axis_y_values = true; + bool option_show_average_line = false; + float axis_x_unit = 1; + std::string axis_x_unit_name = "", axis_y_unit_name = ""; + + operator bool() const; + bool operator==(const PlotParam& prev) = delete; + bool operator!=(const PlotParam& prev) = delete; + bool reuse_graph(const PlotParam& prev) const; + bool reuse_data(const PlotParam& prev) const; + + IndexRange data_range_x() const; //the part of range_x currently available + + float alloc_x_step() const; + AxisRange alloc_x() const; + AxisRange alloc_y() const; +}; + +class PlotBuffer +{ +public: + PlotBuffer(); void init(CircularBuffer* src, unsigned int cnt_limit = 0); + PlotBuffer(CircularBuffer* src, unsigned int cnt_limit = 0); + ~PlotBuffer(); + + const PlotParam& get_param() const; + bool sync(const PlotParam& param, bool forced_sync = false); + void cairo_load(const Cairo::RefPtr& cr, bool forced_redraw = false); + +private: + CircularBuffer* source; + + unsigned long int* buf_spike = NULL; + + unsigned int buf_cr_cnt_max; + cairo_path_data_t* buf_cr = NULL; unsigned int buf_cr_size, i_buf_cr = 1; + cairo_path_data_t* buf_cr_spike = NULL; unsigned int buf_cr_spike_size, i_buf_cr_spike = 1; + + unsigned int cur_buf_cr = 0, cnt_buf_cr = 0; + unsigned int cur_ext = 0, cnt_ext = 0; //don't care if flag_redraw or forced_redraw is set + + PlotParam param; + bool flag_redraw = true; //set by sync(), cleared by cairo_stroke() + float buf_cr_x_step = 0; //set by buf_cr_refresh_x() + + void buf_cr_refresh_x(float x_step); //set all point x values (need to be translated) in the buffer + void buf_cr_load(unsigned int cur_buf_cr, IndexRange range_data); + void buf_cr_spike_sync(); + void buf_cr_add(float y); //it expects i_buf_cr to be an odd index (see cairo_path_data_t reference) + void buf_cr_spike_add(float x, float y); + + unsigned int cur_move(unsigned int cur, int offset) const; + unsigned int i_to_cur(unsigned int i) const; + unsigned int cur_to_i(unsigned int cur) const; //returns an odd index +}; + +class PlotArea: public Gtk::DrawingArea +{ +public: + enum {Plot_Data_Amount_Limit_Min = 512}; + enum {Border_X_Left = 50, Border_Y = 14}; + + PlotArea(); void init(CircularBuffer* buf); + PlotArea(CircularBuffer* buf); + virtual ~PlotArea(); + + // functions below can be called in another thread + + IndexRange get_range_x() const; //current x-axis source data range + ValueRange get_range_y() const; //current y-axis value range + + // forced_check_range_y and forced_adapt make sense when option_auto_set_range_y is set + void refresh(bool forced_check_range_y = false, bool forced_adapt = false, bool forced_sync = false); + bool set_refresh_mode(bool auto_refresh = true, unsigned int interval = 0); //in milliseconds + + // range control + bool set_range_x(IndexRange range); //the only way to change index range width + bool set_range_y_length_min(float length_min); //minimum range length of y-axis range in auto-set mode + void set_option_auto_goto_end(bool set); //set range_x to the end of buffer automatically, default: true + void set_option_auto_extend_range_x(bool set); //extend to show all data in goto-end mode (lower CPU usage), default: false + void set_option_auto_set_range_y(bool set); //set range_y automatically, default: true + void set_option_auto_set_zero_bottom(bool set); //always set bottom of range y to 0 in auto-set mode, default: true + void range_x_goto_end(); void range_x_extend(bool remain_space = true); + bool set_range_y(ValueRange range); void range_y_auto_set(bool adapt = true); + + // grid options + bool set_axis_divider(unsigned int x_div, unsigned int y_div); //how many segments the axis should be divided into + bool set_axis_x_unit(float unit); //it should be the data interval, index values are multiplied by the unit + void set_axis_x_unit_name(std::string str_unit); //short name is expected; it's shown when option_show_axis_x_values is set + void set_axis_y_unit_name(std::string str_unit); //it should be as short as possible; when option_show_axis_y_values is set + void set_option_fixed_scale(bool set); //default: true. note: tick values of fixed scale can have many decimal digits + void set_option_show_axis_x_values(bool set); //show tick values at the bottom, default: true + void set_option_axis_x_int_values(bool set); //remove decimal digits in x-axis tick values, default: false + void set_option_show_axis_y_values(bool set); //show tick values left of y-axis, default: true + void set_option_show_average_line(bool set); //this requires extra calculation (optimized), default: false + + // plotting style options + void set_plot_color(Gdk::RGBA color); + void set_option_anti_alias(bool set); + +private: + CircularBuffer* source = NULL; //data source + PlotBuffer buf_plot; // used for buffering the cairo path data + + UIntRange plot_data_amount_max_range = UIntRange(Plot_Data_Amount_Limit_Min, 2048); //adjust range + + IndexRange range_x = IndexRange(0, 100); + float range_y_length_min = 0; + PlotParam param; + + volatile bool option_auto_goto_end = true; + volatile bool option_auto_extend_range_x = false; + + bool option_auto_set_range_y = true; + bool option_auto_set_zero_bottom = true; + + // used for controlling the interval of range y auto setting + unsigned int counter1 = 0, counter2 = 0; + volatile bool flag_check_range_y = false, flag_adapt = false, flag_sync = false; + + std::ostringstream oss; //used for printing value labels for the grid + const std::vector dash_pattern = {10, 2, 2, 2}; //used for drawing average line + bool flag_set_colors = true; //set background/grid/text colors on first signal_size_allocation + Gdk::RGBA color_back, color_grid, color_text; + + Glib::Dispatcher dispatcher; //used for accepting refresh request from another thread + volatile bool flag_drawing = false; + // used for auto-refresh mode + std::thread* thread_timer; + volatile bool flag_auto_refresh = false; + unsigned int refresh_interval = 40; //25 Hz + + void on_style_updated() override; + void on_size_allocation(Gtk::Allocation& allocation); + void adjust_index_step(); + bool on_draw(const Cairo::RefPtr& cr) override; + + void refresh_loop(); //for auto-refresh mode + + void draw(Cairo::RefPtr cr = (Cairo::RefPtr)nullptr); + void draw_grid(Cairo::RefPtr cr, const PlotParam& param, bool not_erase = true); +}; + +inline IndexRange PlotArea::get_range_x() const +{ + return this->range_x; +} + +inline ValueRange PlotArea::get_range_y() const +{ + return this->param.range_y; +} + +/*------------------------------ PlotParam, PlotBuffer functions ------------------------------*/ + +inline PlotParam::operator bool() const +{ + return data_cnt > 1 + && index_step > 0 + && range_x.count_by_step(index_step) > 1 + && range_y.length() > 0 + && !alloc.has_zero_area(); +} + +inline IndexRange PlotParam::data_range_x() const +{ + if (this->data_cnt_overall == 0 || this->data_cnt == 0) return IndexRange(); + IndexRange available(0, this->data_cnt - 1); + available.max_move_to(this->data_cnt_overall - 1); + return intersection(this->range_x, available); +} + +inline float PlotParam::alloc_x_step() const +{ + if (this->range_x.length() == 0) return 0; + return ((float)this->alloc.get_width() / this->range_x.length()) * this->index_step; +} + +inline AxisRange PlotParam::alloc_x() const +{ + return AxisRange(this->alloc.get_x(), this->alloc.get_x() + this->alloc.get_width()); +} + +inline AxisRange PlotParam::alloc_y() const +{ + return AxisRange(this->alloc.get_y(), this->alloc.get_y() + this->alloc.get_height()); +} + +inline const PlotParam& PlotBuffer::get_param() const +{ + return this->param; +} + +inline void PlotBuffer::buf_cr_add(float y) +{ + this->buf_cr[this->i_buf_cr].point.y = y; + this->i_buf_cr += 2; + if (this->i_buf_cr >= this->buf_cr_size) + this->i_buf_cr -= this->buf_cr_size; +} + +inline void PlotBuffer::buf_cr_spike_add(float x, float y) +{ + this->buf_cr_spike[this->i_buf_cr_spike].point.x = x; + this->buf_cr_spike[this->i_buf_cr_spike].point.y = y; + this->i_buf_cr_spike += 2; +} + +inline unsigned int PlotBuffer::cur_move(unsigned int cur, int offset) const +{ + int cur_new = (int)cur + offset; + while (cur_new < 0) + cur_new += this->buf_cr_cnt_max; + while (cur_new >= this->buf_cr_cnt_max) + cur_new -= this->buf_cr_cnt_max; + return cur_new; +} + +inline unsigned int PlotBuffer::i_to_cur(unsigned int i) const +{ + return i / 2; +} + +inline unsigned int PlotBuffer::cur_to_i(unsigned int cur) const +{ + return 2*cur + 1; +} + +} +#endif + diff --git a/recorder.cpp b/recorder.cpp index d0740ae..32eb5dc 100644 --- a/recorder.cpp +++ b/recorder.cpp @@ -10,6 +10,7 @@ #include //put_time() #include #include + #include #include @@ -23,14 +24,14 @@ namespace SimpleCairoPlot { Recorder::Recorder(): Box(Gtk::ORIENTATION_VERTICAL, 5), - scrollbox(Gtk::ORIENTATION_HORIZONTAL, PlottingArea::Border_X_Left - 2), + scrollbox(Gtk::ORIENTATION_HORIZONTAL, PlotArea::Border_X_Left - 2), scrollbar(Gtk::Adjustment::create(0, 0, 200, 1, 200, 200), Gtk::ORIENTATION_HORIZONTAL), box_var_names(Gtk::ORIENTATION_HORIZONTAL, 20) {} Recorder::Recorder(std::vector& ptrs, unsigned int buf_size): Box(Gtk::ORIENTATION_VERTICAL, 5), - scrollbox(Gtk::ORIENTATION_HORIZONTAL, PlottingArea::Border_X_Left - 2), + scrollbox(Gtk::ORIENTATION_HORIZONTAL, PlotArea::Border_X_Left - 2), scrollbar(Gtk::Adjustment::create(0, 0, 200, 1, 200, 200), Gtk::ORIENTATION_HORIZONTAL), box_var_names(Gtk::ORIENTATION_HORIZONTAL, 20) { @@ -47,13 +48,13 @@ void Recorder::init(std::vector& ptrs, unsigned int buf_size) throw std::invalid_argument("Recorder::init(): requires at least 1 VariableAccessPtr."); this->var_cnt = ptrs.size(); - this->flag_spike_check = (buf_size > Plot_Data_Amount_Limit_Min); + this->flag_spike_check = (buf_size > PlotArea::Plot_Data_Amount_Limit_Min); bool except_caught = false; try { this->ptrs = new VariableAccessPtr[var_cnt]; this->bufs = new CircularBuffer[var_cnt]; - this->areas = new PlottingArea[var_cnt]; + this->areas = new PlotArea[var_cnt]; this->eventboxes = new Gtk::EventBox[var_cnt]; this->var_labels = new Gtk::Label[var_cnt]; @@ -74,31 +75,31 @@ void Recorder::init(std::vector& ptrs, unsigned int buf_size) throw std::bad_alloc(); } - sigc::slot slot_press = sigc::mem_fun(*this, &Recorder::on_button_press); + sigc::slot slot_click = sigc::mem_fun(*this, &Recorder::on_mouse_click); sigc::slot slot_motion = sigc::mem_fun(*this, &Recorder::on_motion_notify); sigc::slot slot_leave = sigc::mem_fun(*this, &Recorder::on_leave_notify); for (unsigned int i = 0; i < this->var_cnt; i++) { if (this->flag_spike_check) this->bufs[i].set_spike_check_ref_min(100.0 * pow(0.1, this->ptrs[i].precision_csv)); - - this->areas[i].axis_y_unit_name = this->ptrs[i].unit_name; - this->areas[i].color_plot = this->ptrs[i].color_plot; + + this->areas[i].set_axis_y_unit_name(this->ptrs[i].unit_name); + this->areas[i].set_plot_color(this->ptrs[i].color_plot); this->eventboxes[i].add(this->areas[i]); this->eventboxes[i].set_events(Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK); - this->eventboxes[i].signal_button_press_event().connect(slot_press); + this->eventboxes[i].signal_button_release_event().connect(slot_click); this->eventboxes[i].signal_motion_notify_event().connect(slot_motion); this->eventboxes[i].signal_leave_notify_event().connect(slot_leave); this->pack_start(this->eventboxes[i], Gtk::PACK_EXPAND_WIDGET); if (i < this->var_cnt - 1) { - this->areas[i].option_show_axis_x_values = false; + this->areas[i].set_option_show_axis_x_values(false); Gtk::Separator* separator = Gtk::manage(new Gtk::Separator); Gtk::Box* box_separator = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); box_separator->pack_start(*separator, Gtk::PACK_SHRINK); - separator->set_size_request(PlottingArea::Border_X_Left, 2); + separator->set_size_request(PlotArea::Border_X_Left, 2); this->pack_start(*box_separator, Gtk::PACK_SHRINK); } @@ -185,6 +186,7 @@ void Recorder::stop() void Recorder::clear() { this->flag_full = false; + this->flag_sync_buf_plot = true; for (unsigned int i = 0; i < this->var_cnt; i++) this->bufs[i].clear(true); } @@ -310,8 +312,8 @@ bool Recorder::set_redraw_interval(unsigned new_redraw_interval) { if (new_redraw_interval == 0) return false; this->redraw_interval = new_redraw_interval; - if (this->redraw_interval < 20) - this->redraw_interval = 20; //maximum graph refresh rate: 50 Hz + if (this->redraw_interval < 40) + this->redraw_interval = 40; //maximum graph refresh rate: 25 Hz return true; } @@ -347,8 +349,8 @@ bool Recorder::set_index_unit(float unit) this->axis_x_unit_name = sst.str() + " s"; } - this->areas[this->var_cnt - 1].axis_x_unit_name - = (this->flag_axis_x_unique_unit? "" : this->axis_x_unit_name); + this->areas[this->var_cnt - 1].set_axis_x_unit_name + (this->flag_axis_x_unique_unit? "" : this->axis_x_unit_name); this->label_axis_x_unit.set_visible(this->flag_axis_x_unique_unit); if (this->flag_axis_x_unique_unit) @@ -357,16 +359,14 @@ bool Recorder::set_index_unit(float unit) return true; } -bool Recorder::set_axis_x_range(AxisRange range) +bool Recorder::set_axis_x_range(IndexRange range) { if (! this->var_cnt) return false; - range.set_int(); - if (range.length() > this->data_range_max().length()) return false; - - if (range.length() == 0) + if (range.count() > this->data_range_max().count()) return false; + if (!range || range.count() < 2) range = this->axis_x_range(); - else if (range.length() == this->data_count_max()) + else if (range.count() == this->data_count_max() + 1) //careless of the caller range.set(0, range.max() - 1); else range.fit_by_range(this->data_range_max()); @@ -375,17 +375,17 @@ bool Recorder::set_axis_x_range(AxisRange range) this->areas[i].set_range_x(range); if (this->flag_recording && !this->flag_full) - this->auto_set_scroll_mode - (Gtk::Adjustment::create(range.min(), 0, this->data_range().max(), 1, 1, range.length())); + this->auto_set_scroll_mode(Gtk::Adjustment::create( + range.min(), 0, this->data_range().max(), 1, 1, range.length())); this->refresh_areas(true, true); - this->flag_refresh_scroll = true; //tells refresh_indicators() that the scroll needs to be updated + this->flag_refresh_scroll = true; //tells refresh_indicators() the scroll needs to be updated this->dispatcher_refresh_indicators.emit(); return true; } -bool Recorder::set_axis_y_range(unsigned int index, AxisRange range) +bool Recorder::set_axis_y_range(unsigned int index, ValueRange range) { if (index > this->var_cnt - 1) return false; return this->areas[index].set_range_y(range); @@ -394,7 +394,7 @@ bool Recorder::set_axis_y_range(unsigned int index, AxisRange range) bool Recorder::set_axis_y_range_length_min(unsigned int index, float length_min) { if (index > this->var_cnt - 1) return false; - return this->areas[index].set_axis_y_range_length_min(length_min); + return this->areas[index].set_range_y_length_min(length_min); } bool Recorder::set_axis_divider(unsigned int x_div, unsigned int y_div) @@ -408,7 +408,7 @@ bool Recorder::set_axis_divider(unsigned int x_div, unsigned int y_div) void Recorder::set_option_fixed_axis_scale(bool set) { for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].option_fixed_scale = set; + this->areas[i].set_option_fixed_scale(set); } void Recorder::set_option_stop_on_full(bool set) @@ -426,44 +426,44 @@ void Recorder::set_option_auto_extend_range_x(bool set) void Recorder::set_option_auto_set_range_y(unsigned int index, bool set) { if (index > this->var_cnt - 1) return; - this->areas[index].option_auto_set_range_y = set; + this->areas[index].set_option_auto_set_range_y(set); } void Recorder::set_option_auto_set_zero_bottom(unsigned int index, bool set) { if (index > this->var_cnt - 1) return; - this->areas[index].option_auto_set_zero_bottom = set; + this->areas[index].set_option_auto_set_zero_bottom(set); } void Recorder::set_option_show_axis_x_values(bool set) { if (! this->var_cnt) return; - this->areas[this->var_cnt - 1].option_auto_set_range_y = set; + this->areas[this->var_cnt - 1].set_option_auto_set_range_y(set); } void Recorder::set_option_axis_x_int_values(bool set) { if (! this->var_cnt) return; - this->areas[this->var_cnt - 1].option_axis_x_int_values = set; + this->areas[this->var_cnt - 1].set_option_axis_x_int_values(set); } void Recorder::set_option_show_axis_y_values(bool set) { for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].option_show_axis_y_values = set; + this->areas[i].set_option_show_axis_y_values(set); } void Recorder::set_option_show_average_line(unsigned int index, bool set) { if (index > this->var_cnt - 1) return; - this->areas[index].option_show_average_line = set; + this->areas[index].set_option_show_average_line(set); } void Recorder::set_option_anti_alias(bool set) { for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].option_anti_alias = set; + this->areas[i].set_option_anti_alias(set); } /*------------------------------ private functions ------------------------------*/ @@ -520,7 +520,7 @@ void Recorder::refresh_loop() if (steady_clock::now() >= t_last_refresh + milliseconds(this->redraw_interval)) { // refresh graph view t_last_refresh = steady_clock::now(); - this->refresh_areas(); + this->refresh_areas(); //auto-refresh mode of areas aren't set, combining them into one thread } if (!this->flag_full || this->flag_cursor) @@ -538,7 +538,7 @@ void Recorder::refresh_indicators() //not thread-safe if (this->flag_full && !this->flag_refresh_scroll) return; Glib::RefPtr adj = this->scrollbar.get_adjustment(); - AxisRange range_x = this->axis_x_range(); + IndexRange range_x = this->axis_x_range(); unsigned int val, upper = this->data_range().max(); if (this->flag_goto_end && !flag_extend && range_x.max() < upper) @@ -546,7 +546,7 @@ void Recorder::refresh_indicators() //not thread-safe else val = range_x.min(); - // tell on_scroll() don't refresh areas again if the range has been moved by PlottingArea + // tell on_scroll() don't refresh areas again if the range has been moved by PlotArea this->flag_refresh_scroll = true; adj->configure(val, 0, upper, 1, range_x.length() / 2, range_x.length()); this->flag_refresh_scroll = false; @@ -562,30 +562,30 @@ void Recorder::on_scroll() //on scrollbar.signal_value_changed() Glib::RefPtr adj = this->scrollbar.get_adjustment(); unsigned int val = adj->get_value(); for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].set_range_x(AxisRange(val, val + adj->get_page_size())); + this->areas[i].set_range_x(IndexRange(val, val + adj->get_page_size())); } if (!this->flag_recording || this->interval >= 100) this->refresh_areas(true); } -bool Recorder::on_button_press(GdkEventButton* event) +bool Recorder::on_mouse_click(GdkEventButton* event) { if (this->data_count() == 0) return true; - if (event->type != GDK_BUTTON_PRESS) return true; + if (event->type != GDK_BUTTON_RELEASE) return true; if (event->button != 1 && event->button != 3) return true; bool zoom_in = (event->button == 1); //is left button? if (!zoom_in && this->flag_extend) return true; - AxisRange range_scr_x(PlottingArea::Border_X_Left, + AxisRange range_scr_x(PlotArea::Border_X_Left, this->areas[0].get_allocation().get_width()); AxisRange range_x = this->axis_x_range(); unsigned int x = range_scr_x.map(event->x, range_x); if (zoom_in) { - range_x.scale(0.5, x); range_x.set_int(); + range_x.scale(0.5, x); range_x = UIntRange(range_x); if (range_x.length() < 2) return true; } else range_x.scale(2, x); @@ -610,14 +610,14 @@ bool Recorder::auto_set_scroll_mode(Glib::RefPtr adj) //default this->flag_goto_end = (adj->get_value() + adj->get_page_size() >= adj->get_upper()); for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].option_auto_goto_end = this->flag_goto_end; + this->areas[i].set_option_auto_goto_end(this->flag_goto_end); if (this->flag_goto_end) { bool wide_enough = (adj->get_page_size() >= adj->get_upper()); this->flag_extend = this->option_extend_index_range && wide_enough; for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].option_auto_extend_range_x = this->flag_extend; + this->areas[i].set_option_auto_extend_range_x(this->flag_extend); } else this->flag_extend = false; @@ -632,7 +632,7 @@ inline std::string float_to_str(float val, std::ostringstream& oss) bool Recorder::on_motion_notify(GdkEventMotion* motion_event) { - this->flag_cursor = (this->cursor_x >= PlottingArea::Border_X_Left); + this->flag_cursor = (this->cursor_x >= PlotArea::Border_X_Left); this->cursor_x = motion_event->x; this->refresh_var_labels(); return true; @@ -649,7 +649,7 @@ void Recorder::refresh_var_labels() { float x; bool show_values = false; if (this->flag_cursor) { - AxisRange range_scr_x(PlottingArea::Border_X_Left, + AxisRange range_scr_x(PlotArea::Border_X_Left, this->areas[0].get_allocation().get_width()); x = range_scr_x.map(this->cursor_x, this->axis_x_range()); show_values = this->data_range().contain(x); diff --git a/recorder.h b/recorder.h index c5ba52c..49a203b 100644 --- a/recorder.h +++ b/recorder.h @@ -15,25 +15,18 @@ #include #include -#include +#include namespace SimpleCairoPlot { +class Recorder; class VariableAccessPtr; +using RecordView = Recorder; using VariableAccessFuncPtr = float (*)(void*); // used by the recorder to access current values of variables class VariableAccessPtr { - bool is_func_ptr = false; - - // used if it's a data pointer - const float* addr_data = NULL; - - // used if it's a function pointer - VariableAccessFuncPtr addr_func = NULL; - void* addr_obj = NULL; - public: std::string unit_name = "", name_csv = ""; Glib::ustring name_friendly = ""; @@ -48,6 +41,16 @@ class VariableAccessPtr void set(const float* pd); void set(void* pobj, VariableAccessFuncPtr pfunc); float read() const; + +private: + bool is_func_ptr = false; + + // used if it's a data pointer + const float* addr_data = NULL; + + // used if it's a function pointer + VariableAccessFuncPtr addr_func = NULL; + void* addr_obj = NULL; }; // this function is regenerated at different address for each class at compile time, @@ -105,52 +108,6 @@ extern const std::string Empty_Comment; class Recorder: public Gtk::Box { - unsigned int var_cnt = 0; - - VariableAccessPtr* ptrs = NULL; - CircularBuffer* bufs = NULL; - PlottingArea* areas = NULL; Gtk::EventBox* eventboxes = NULL; - - Gtk::Box scrollbox; Gtk::Scrollbar scrollbar; Gtk::Label space_left_of_scroll; - Gtk::Box box_var_names; Gtk::Label* var_labels; Gtk::Label label_cursor_x, label_axis_x_unit; - Glib::Dispatcher dispatcher_refresh_indicators; volatile bool flag_refresh_scroll = false; - - std::thread* thread_record = NULL, - * thread_refresh = NULL; - volatile bool flag_recording = false; - bool flag_spike_check = false; //determined by buf_size > Plot_Data_Amount_Limit_Min - bool option_stop_on_full = false; - float interval = 10; unsigned int redraw_interval = 20; //in milliseconds - std::chrono::system_clock::time_point tp_start; - - bool option_extend_index_range = false; - volatile bool flag_goto_end = false, flag_extend = false; - - volatile bool flag_full = false; - sigc::signal sig_full; - Glib::Dispatcher dispatcher_sig_full; - - float axis_x_unit = 0; bool flag_axis_x_unique_unit = false; - std::string axis_x_unit_name = ""; - - volatile bool flag_cursor = false; volatile float cursor_x = 0; - std::ostringstream oss; //used to show x,y values at the cursor's location - - void record_loop(); - void refresh_loop(); - - void on_scroll(); - bool on_button_press(GdkEventButton* event); - bool on_motion_notify(GdkEventMotion* motion_event); - bool on_leave_notify(GdkEventCrossing* crossing_event); - - void refresh_indicators(); - void refresh_areas(bool forced_check_range_y = false, bool forced_adapt = false); - bool auto_set_scroll_mode(); - bool auto_set_scroll_mode(Glib::RefPtr adj); - - void refresh_var_labels(); - public: // note: some inline functions are NOT safe before initialization Recorder(); void init(std::vector& ptrs, unsigned int buf_size); @@ -160,8 +117,8 @@ class Recorder: public Gtk::Box bool is_recording() const; float data_interval() const; unsigned int var_count() const; - unsigned int data_count() const; AxisRange data_range() const; - unsigned int data_count_max() const; AxisRange data_range_max() const; + unsigned int data_count() const; IndexRange data_range() const; + unsigned int data_count_max() const; IndexRange data_range_max() const; float t_data(unsigned int i) const; //the unit is determined by set_index_unit() (default: s) float t_first_data() const; @@ -175,8 +132,8 @@ class Recorder: public Gtk::Box // into the buffers, and call set_axis_x_range() at last to show them. CircularBuffer& buffer(unsigned int index) const; - AxisRange axis_x_range() const; //index range in the buffers - AxisRange axis_y_range(unsigned int index) const; + IndexRange axis_x_range() const; //index range in the buffers + ValueRange axis_y_range(unsigned int index) const; bool start(); //clears the buffers void stop(); @@ -187,17 +144,17 @@ class Recorder: public Gtk::Box bool open_csv(const std::string& file_path); //note: comments will not be loaded bool save_csv(const std::string& file_path, const std::string& str_comment = Empty_Comment); //note: comment is unstandard - void refresh_view(); //call this function if data has been loaded into the buffers manually + void refresh_view(); //call this function if new data has been loaded into the buffers manually bool set_interval(float new_interval); //interval of reading current values (ms). it sets index unit (multipier) to interval (s) bool set_redraw_interval(unsigned int new_redraw_interval); //set manually if a slower redraw rate is required to reduce CPU usage bool set_index_unit(float unit); //note: set to interval in ms, s (default), min or h. index values are multiplied by the unit - bool set_axis_x_range(AxisRange range); //range.width() + 1 is the amount of data shows in each area - bool set_axis_x_range(unsigned int range_width = 0); //equal to AxisRange(0, range_width) except in goto-end mode - bool set_axis_x_range(unsigned int min, unsigned int max); //equal to AxisRange(min, max) - bool set_axis_y_range(unsigned int index, AxisRange range); //useless when option_auto_set_range_y is set + bool set_axis_x_range(IndexRange range); //range.width() + 1 is the amount of data shows in each area + bool set_axis_x_range(unsigned int range_width = 0); //equal to IndexRange(0, range_width) except in goto-end mode + bool set_axis_x_range(unsigned int min, unsigned int max); //equal to IndexRange(min, max) + bool set_axis_y_range(unsigned int index, ValueRange range); //useless when option_auto_set_range_y is set bool set_axis_y_range_length_min(unsigned int index, float length_min); //minimum range length of y-axis range in auto-set mode bool set_axis_divider(unsigned int x_div, unsigned int y_div); //how many segments the axis should be divided into @@ -216,10 +173,57 @@ class Recorder: public Gtk::Box void set_option_show_average_line(unsigned int index, bool set); //this requires extra calculation, though it was optimized void set_option_anti_alias(bool set); //font of x-axis, y-axis values are not influenced. default: false + +private: + unsigned int var_cnt = 0; + + VariableAccessPtr* ptrs = NULL; + CircularBuffer* bufs = NULL; + PlotArea* areas = NULL; Gtk::EventBox* eventboxes = NULL; //DrawingArea can't handle button events anyway + + Gtk::Box scrollbox; Gtk::Scrollbar scrollbar; Gtk::Label space_left_of_scroll; + Gtk::Box box_var_names; Gtk::Label* var_labels; Gtk::Label label_cursor_x, label_axis_x_unit; + Glib::Dispatcher dispatcher_refresh_indicators; volatile bool flag_refresh_scroll = false; + + std::thread* thread_record = NULL, + * thread_refresh = NULL; + volatile bool flag_recording = false; + bool flag_spike_check = false; //determined by buf_size > Plot_Data_Amount_Limit_Min + bool option_stop_on_full = false; + float interval = 10; unsigned int redraw_interval = 40; //in milliseconds + std::chrono::system_clock::time_point tp_start; + + bool option_extend_index_range = false; + volatile bool flag_goto_end = false, flag_extend = false; + + volatile bool flag_full = false; + sigc::signal sig_full; + Glib::Dispatcher dispatcher_sig_full; + + bool flag_sync_buf_plot = false; + + float axis_x_unit = 0; bool flag_axis_x_unique_unit = false; + std::string axis_x_unit_name = ""; + + volatile bool flag_cursor = false; volatile float cursor_x = 0; + std::ostringstream oss; //used to show x,y values at the cursor's location + + void record_loop(); + void refresh_loop(); + + void on_scroll(); + bool on_mouse_click(GdkEventButton* event); + bool on_motion_notify(GdkEventMotion* motion_event); + bool on_leave_notify(GdkEventCrossing* crossing_event); + + void refresh_indicators(); + void refresh_areas(bool forced_check_range_y = false, bool forced_adapt = false); + bool auto_set_scroll_mode(); + bool auto_set_scroll_mode(Glib::RefPtr adj); + + void refresh_var_labels(); }; -using RecordView = Recorder; - inline bool Recorder::is_recording() const { return this->flag_recording; @@ -240,7 +244,7 @@ inline unsigned int Recorder::data_count() const return this->bufs[0].count(); } -inline AxisRange Recorder::data_range() const +inline IndexRange Recorder::data_range() const { return this->bufs[0].range(); } @@ -250,7 +254,7 @@ inline unsigned int Recorder::data_count_max() const return this->bufs[0].size(); } -inline AxisRange Recorder::data_range_max() const +inline IndexRange Recorder::data_range_max() const { return this->bufs[0].range_max(); } @@ -297,14 +301,14 @@ inline CircularBuffer& Recorder::buffer(unsigned int index) const return this->bufs[index]; } -inline AxisRange Recorder::axis_x_range() const +inline IndexRange Recorder::axis_x_range() const { return this->areas[0].get_range_x(); } -inline AxisRange Recorder::axis_y_range(unsigned int index) const +inline ValueRange Recorder::axis_y_range(unsigned int index) const { - if (index > this->var_cnt - 1) return AxisRange(0, 0); + if (index > this->var_cnt - 1) return ValueRange(0, 0); return this->areas[index].get_range_y(); } @@ -315,7 +319,7 @@ inline void Recorder::refresh_view() inline bool Recorder::set_axis_x_range(unsigned int range_width) { - AxisRange range(0, range_width); + IndexRange range(0, range_width); if (this->flag_goto_end && range_width < this->data_range().max()) range.max_move_to(this->data_range().max()); return this->set_axis_x_range(range); @@ -323,10 +327,11 @@ inline bool Recorder::set_axis_x_range(unsigned int range_width) inline bool Recorder::set_axis_x_range(unsigned int min, unsigned int max) { - return this->set_axis_x_range(AxisRange(min, max)); + return this->set_axis_x_range(IndexRange(min, max)); } -// private +/*------------------------------ private functions ------------------------------*/ + inline bool Recorder::auto_set_scroll_mode() { return this->auto_set_scroll_mode(this->scrollbar.get_adjustment()); @@ -335,7 +340,8 @@ inline bool Recorder::auto_set_scroll_mode() inline void Recorder::refresh_areas(bool forced_check_range_y, bool forced_adapt) { for (unsigned int i = 0; i < this->var_cnt; i++) - this->areas[i].refresh(forced_check_range_y, forced_adapt); + this->areas[i].refresh(forced_check_range_y, forced_adapt, this->flag_sync_buf_plot); + this->flag_sync_buf_plot = false; } }