summaryrefslogtreecommitdiffstats
path: root/src/spectro_source.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/spectro_source.cc')
-rw-r--r--src/spectro_source.cc615
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];
+}