diff options
Diffstat (limited to '')
-rw-r--r-- | src/spectro_source.cc | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/src/spectro_source.cc b/src/spectro_source.cc new file mode 100644 index 0000000..e0dc802 --- /dev/null +++ b/src/spectro_source.cc @@ -0,0 +1,615 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file spectro_source.cc + */ + +#include "spectro_source.hh" + +#include "base/ansi_scrubber.hh" +#include "base/math_util.hh" +#include "command_executor.hh" +#include "config.h" + +nonstd::optional<size_t> +spectrogram_row::nearest_column(size_t current) const +{ + nonstd::optional<size_t> retval; + nonstd::optional<size_t> nearest_distance; + + for (size_t lpc = 0; lpc < this->sr_width; lpc++) { + if (this->sr_values[lpc].rb_counter == 0) { + continue; + } + auto curr_distance = abs_diff(lpc, current); + + if (!retval || curr_distance < nearest_distance.value()) { + retval = lpc; + nearest_distance = abs_diff(lpc, current); + } + } + + return retval; +} + +bool +spectrogram_source::list_input_handle_key(listview_curses& lv, int ch) +{ + switch (ch) { + case 'm': { + auto sel = lv.get_selection(); + if (sel < 0 || (size_t) sel >= this->text_line_count() + || !this->ss_cursor_column || this->ss_value_source == nullptr) + { + alerter::singleton().chime( + "a value must be selected before it can be marked"); + return true; + } + + unsigned long width; + vis_line_t height; + + lv.get_dimensions(height, width); + width -= 2; + + auto& sb = this->ss_cached_bounds; + auto begin_time_opt = this->time_for_row_int(sel); + if (!begin_time_opt) { + return true; + } + auto begin_time = begin_time_opt.value(); + struct timeval end_time = begin_time; + + end_time.tv_sec += this->ss_granularity; + double range_min, range_max, column_size; + + column_size = (sb.sb_max_value_out - sb.sb_min_value_out) + / (double) (width - 1); + range_min = sb.sb_min_value_out + + this->ss_cursor_column.value_or(0) * column_size; + range_max = range_min + column_size; + this->ss_value_source->spectro_mark((textview_curses&) lv, + begin_time.tv_sec, + end_time.tv_sec, + range_min, + range_max); + this->invalidate(); + lv.reload_data(); + return true; + } + + case KEY_CTRL_A: { + if (this->ss_value_source != nullptr) { + this->ss_cursor_column = 0; + this->text_selection_changed((textview_curses&) lv); + lv.set_needs_update(); + } + return true; + } + + case KEY_CTRL_E: { + if (this->ss_value_source != nullptr) { + this->ss_cursor_column = INT_MAX; + this->text_selection_changed((textview_curses&) lv); + lv.set_needs_update(); + } + return true; + } + + case KEY_LEFT: + case KEY_RIGHT: { + auto sel = lv.get_selection(); + unsigned long width; + vis_line_t height; + string_attrs_t sa; + + lv.get_dimensions(height, width); + + this->text_attrs_for_line((textview_curses&) lv, sel, sa); + + if (sa.empty()) { + this->ss_cursor_column = nonstd::nullopt; + return true; + } + + if (!this->ss_cursor_column) { + lv.set_selection(0_vl); + } + struct line_range lr(this->ss_cursor_column.value(), + this->ss_cursor_column.value() + 1); + + auto current = find_string_attr(sa, lr); + + if (current != sa.end()) { + if (ch == KEY_LEFT) { + if (current == sa.begin()) { + current = sa.end(); + } else { + --current; + } + } else { + ++current; + } + } + + if (current == sa.end()) { + if (ch == KEY_LEFT) { + current = sa.end(); + --current; + } else { + current = sa.begin(); + } + } + this->ss_cursor_column = current->sa_range.lr_start; + + lv.reload_data(); + + return true; + } + default: + return false; + } +} + +bool +spectrogram_source::list_value_for_overlay(const listview_curses& lv, + int y, + int bottom, + vis_line_t row, + attr_line_t& value_out) +{ + vis_line_t height; + unsigned long width; + + lv.get_dimensions(height, width); + width -= 2; + + if (y > 0) { + auto sel = lv.get_selection(); + auto selected_y = sel - lv.get_top() + 2; + + if (y == selected_y && this->ss_cursor_column) { + const auto& s_row = this->load_row(lv, sel); + const auto& bucket + = s_row.sr_values[this->ss_cursor_column.value()]; + auto& sb = this->ss_cached_bounds; + spectrogram_request sr(sb); + + auto sel_time = rounddown(sb.sb_begin_time, this->ss_granularity) + + sel * this->ss_granularity; + sr.sr_width = width; + sr.sr_begin_time = sel_time; + sr.sr_end_time = sel_time + this->ss_granularity; + sr.sr_column_size = (sb.sb_max_value_out - sb.sb_min_value_out) + / (double) (width - 1); + auto range_min = sb.sb_min_value_out + + this->ss_cursor_column.value() * sr.sr_column_size; + auto range_max = range_min + sr.sr_column_size; + + auto desc + = attr_line_t() + .append(lnav::roles::number( + fmt::to_string(bucket.rb_counter))) + .append(fmt::format(FMT_STRING(" value{} in the range "), + bucket.rb_counter == 1 ? "" : "s")) + .append(lnav::roles::number( + fmt::format(FMT_STRING("{:.2Lf}"), range_min))) + .append("-") + .append(lnav::roles::number( + fmt::format(FMT_STRING("{:.2Lf}"), range_max))) + .append(" "); + auto mark_offset = this->ss_cursor_column.value(); + auto mark_is_before = true; + + value_out.al_attrs.emplace_back( + line_range{0, -1}, VC_ROLE.value(role_t::VCR_STATUS_INFO)); + if (desc.length() + 8 > width) { + desc.clear(); + } + + if (this->ss_cursor_column.value() + desc.length() + 1 > width) { + mark_offset -= desc.length(); + mark_is_before = false; + } + value_out.append(mark_offset, ' '); + if (mark_is_before) { + value_out.append("\u25b2 "); + } + value_out.append(desc); + if (!mark_is_before) { + value_out.append("\u25b2 "); + } + + if (this->ss_details_view != nullptr) { + if (s_row.sr_details_source_provider) { + auto row_details_source = s_row.sr_details_source_provider( + sr, range_min, range_max); + + this->ss_details_view->set_sub_source( + row_details_source.get()); + this->ss_details_source = std::move(row_details_source); + auto* overlay_source = dynamic_cast<list_overlay_source*>( + this->ss_details_source.get()); + if (overlay_source != nullptr) { + this->ss_details_view->set_overlay_source( + overlay_source); + } + } else { + this->ss_details_view->set_sub_source( + this->ss_no_details_source); + this->ss_details_view->set_overlay_source(nullptr); + } + } + return true; + } + + return false; + } + + auto& line = value_out.get_string(); + char buf[128]; + + this->cache_bounds(); + + if (this->ss_cached_line_count == 0) { + value_out + .append(lnav::roles::error("error: no data available, use the ")) + .append_quoted(lnav::roles::keyword(":spectrogram")) + .append(lnav::roles::error(" command to visualize numeric data")); + return true; + } + + auto& sb = this->ss_cached_bounds; + auto& st = this->ss_cached_thresholds; + + snprintf(buf, sizeof(buf), "Min: %'.10lg", sb.sb_min_value_out); + line = buf; + + snprintf(buf, + sizeof(buf), + ANSI_ROLE(" ") " 1-%'d " ANSI_ROLE(" ") " %'d-%'d " ANSI_ROLE( + " ") " %'d+", + role_t::VCR_LOW_THRESHOLD, + st.st_green_threshold - 1, + role_t::VCR_MED_THRESHOLD, + st.st_green_threshold, + st.st_yellow_threshold - 1, + role_t::VCR_HIGH_THRESHOLD, + st.st_yellow_threshold); + auto buflen = strlen(buf); + if (line.length() + buflen + 20 < width) { + line.append(width / 2 - buflen / 3 - line.length(), ' '); + } else { + line.append(" "); + } + line.append(buf); + scrub_ansi_string(line, &value_out.get_attrs()); + + snprintf(buf, sizeof(buf), "Max: %'.10lg", sb.sb_max_value_out); + buflen = strlen(buf); + if (line.length() + buflen + 4 < width) { + line.append(width - buflen - line.length() - 2, ' '); + } else { + line.append(" "); + } + line.append(buf); + + value_out.with_attr(string_attr(line_range(0, -1), + VC_STYLE.value(text_attrs{A_UNDERLINE}))); + + return true; +} + +size_t +spectrogram_source::text_line_count() +{ + if (this->ss_value_source == nullptr) { + return 0; + } + + this->cache_bounds(); + + return this->ss_cached_line_count; +} + +size_t +spectrogram_source::text_line_width(textview_curses& tc) +{ + if (tc.get_window() == nullptr) { + return 80; + } + + unsigned long width; + vis_line_t height; + + tc.get_dimensions(height, width); + return width; +} + +nonstd::optional<struct timeval> +spectrogram_source::time_for_row(vis_line_t row) +{ + if (this->ss_details_source != nullptr) { + auto* details_tss = dynamic_cast<text_time_translator*>( + this->ss_details_source.get()); + + if (details_tss != nullptr) { + return details_tss->time_for_row(this->ss_details_view->get_top()); + } + } + + return this->time_for_row_int(row); +} + +nonstd::optional<struct timeval> +spectrogram_source::time_for_row_int(vis_line_t row) +{ + struct timeval retval { + 0, 0 + }; + + this->cache_bounds(); + retval.tv_sec + = rounddown(this->ss_cached_bounds.sb_begin_time, this->ss_granularity) + + row * this->ss_granularity; + + return retval; +} + +nonstd::optional<vis_line_t> +spectrogram_source::row_for_time(struct timeval time_bucket) +{ + if (this->ss_value_source == nullptr) { + return nonstd::nullopt; + } + + time_t diff; + int retval; + + this->cache_bounds(); + auto grain_begin_time + = rounddown(this->ss_cached_bounds.sb_begin_time, this->ss_granularity); + if (time_bucket.tv_sec < grain_begin_time) { + return 0_vl; + } + + diff = time_bucket.tv_sec - grain_begin_time; + retval = diff / this->ss_granularity; + + return vis_line_t(retval); +} + +void +spectrogram_source::text_value_for_line(textview_curses& tc, + int row, + std::string& value_out, + text_sub_source::line_flags_t flags) +{ + const auto& s_row = this->load_row(tc, row); + char tm_buffer[128]; + struct tm tm; + + auto row_time_opt = this->time_for_row_int(vis_line_t(row)); + if (!row_time_opt) { + value_out.clear(); + return; + } + auto row_time = row_time_opt.value(); + + gmtime_r(&row_time.tv_sec, &tm); + strftime(tm_buffer, sizeof(tm_buffer), " %a %b %d %H:%M:%S", &tm); + + value_out = tm_buffer; + value_out.resize(s_row.sr_width, ' '); + + for (size_t lpc = 0; lpc <= s_row.sr_width; lpc++) { + if (s_row.sr_values[lpc].rb_marks) { + value_out[lpc] = 'x'; + } + } +} + +void +spectrogram_source::text_attrs_for_line(textview_curses& tc, + int row, + string_attrs_t& value_out) +{ + if (this->ss_value_source == nullptr) { + return; + } + + const auto& st = this->ss_cached_thresholds; + const auto& s_row = this->load_row(tc, row); + + for (int lpc = 0; lpc <= (int) s_row.sr_width; lpc++) { + int col_value = s_row.sr_values[lpc].rb_counter; + + if (col_value == 0) { + continue; + } + + role_t role; + + if (col_value < st.st_green_threshold) { + role = role_t::VCR_LOW_THRESHOLD; + } else if (col_value < st.st_yellow_threshold) { + role = role_t::VCR_MED_THRESHOLD; + } else { + role = role_t::VCR_HIGH_THRESHOLD; + } + value_out.emplace_back(line_range(lpc, lpc + 1), VC_ROLE.value(role)); + } +} + +void +spectrogram_source::reset_details_source() +{ + if (this->ss_details_view != nullptr) { + this->ss_details_view->set_sub_source(this->ss_no_details_source); + this->ss_details_view->set_overlay_source(nullptr); + } + this->ss_details_source.reset(); +} + +void +spectrogram_source::cache_bounds() +{ + if (this->ss_value_source == nullptr) { + this->ss_cached_bounds.sb_count = 0; + this->ss_cached_bounds.sb_begin_time = 0; + this->ss_cursor_column = nonstd::nullopt; + this->reset_details_source(); + return; + } + + spectrogram_bounds sb; + + this->ss_value_source->spectro_bounds(sb); + + if (sb.sb_count == this->ss_cached_bounds.sb_count) { + return; + } + + this->ss_cached_bounds = sb; + + if (sb.sb_count == 0) { + this->ss_cached_line_count = 0; + this->ss_cursor_column = nonstd::nullopt; + this->reset_details_source(); + return; + } + + time_t grain_begin_time = rounddown(sb.sb_begin_time, this->ss_granularity); + time_t grain_end_time = roundup_size(sb.sb_end_time, this->ss_granularity); + + time_t diff = std::max((time_t) 1, grain_end_time - grain_begin_time); + this->ss_cached_line_count + = (diff + this->ss_granularity - 1) / this->ss_granularity; + + int64_t samples_per_row = sb.sb_count / this->ss_cached_line_count; + auto& st = this->ss_cached_thresholds; + + st.st_yellow_threshold = samples_per_row / 2; + st.st_green_threshold = st.st_yellow_threshold / 2; + + if (st.st_green_threshold <= 1) { + st.st_green_threshold = 2; + } + if (st.st_yellow_threshold <= st.st_green_threshold) { + st.st_yellow_threshold = st.st_green_threshold + 1; + } +} + +const spectrogram_row& +spectrogram_source::load_row(const listview_curses& tc, int row) +{ + this->cache_bounds(); + + unsigned long width; + vis_line_t height; + + tc.get_dimensions(height, width); + width -= 2; + + auto& sb = this->ss_cached_bounds; + spectrogram_request sr(sb); + + sr.sr_width = width; + auto row_time = rounddown(sb.sb_begin_time, this->ss_granularity) + + row * this->ss_granularity; + sr.sr_begin_time = row_time; + sr.sr_end_time = row_time + this->ss_granularity; + + sr.sr_column_size + = (sb.sb_max_value_out - sb.sb_min_value_out) / (double) (width - 1); + + auto& s_row = this->ss_row_cache[row_time]; + + if (s_row.sr_values.empty() || s_row.sr_width != width + || s_row.sr_column_size != sr.sr_column_size) + { + s_row.sr_width = width; + s_row.sr_column_size = sr.sr_column_size; + s_row.sr_values.clear(); + s_row.sr_values.resize(width + 1); + this->ss_value_source->spectro_row(sr, s_row); + } + + return s_row; +} + +bool +spectrogram_source::text_is_row_selectable(textview_curses& tc, vis_line_t row) +{ + if (this->ss_value_source == nullptr) { + return false; + } + + const auto& s_row = this->load_row(tc, row); + auto nearest_column + = s_row.nearest_column(this->ss_cursor_column.value_or(0)); + + return nearest_column.has_value(); +} + +void +spectrogram_source::text_selection_changed(textview_curses& tc) +{ + if (this->ss_value_source == nullptr || this->text_line_count() == 0) { + this->ss_cursor_column = nonstd::nullopt; + return; + } + + const auto& s_row = this->load_row(tc, tc.get_selection()); + this->ss_cursor_column + = s_row.nearest_column(this->ss_cursor_column.value_or(0)); +} + +spectro_status_source::spectro_status_source() +{ + this->sss_fields[F_TITLE].set_width(9); + this->sss_fields[F_TITLE].set_role(role_t::VCR_STATUS_TITLE); + this->sss_fields[F_TITLE].set_value(" Details "); + + this->sss_fields[F_HELP].right_justify(true); + this->sss_fields[F_HELP].set_width(20); + this->sss_fields[F_HELP].set_value("Press " ANSI_BOLD("TAB") " to focus "); + this->sss_fields[F_HELP].set_left_pad(1); +} + +size_t +spectro_status_source::statusview_fields() +{ + return F_MAX; +} + +status_field& +spectro_status_source::statusview_value_for_field(int field) +{ + return this->sss_fields[field]; +} |