#include "Spectrum.h" #include "Curve_Sampling.h" #include "Plottable_Real_Time_Data.h" #include #include #include #include namespace renderive { namespace detail { namespace { struct Spectrum_Frame { std::vector samples; std::vector maxima; std::vector minima; }; struct Spectrum_Interaction_Base {}; struct Spectrum_Interaction { std::vector markers; int selected_marker = -1; Hover_Tooltip_Runtime tooltip; }; using Spectrum_Interaction_State = Double_State_Strategy; RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) { const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin); const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target); const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin); const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target); return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; } void draw_curve(Painter& painter, std::span values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) { auto points = curve_points(values, domain, x_axis, y_axis, visible_only, interpolation); if(points.size() < 2) return; if(brush.enabled()) { std::vector polygon; polygon.reserve(points.size() + 2); polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); polygon.insert(polygon.end(), points.begin(), points.end()); polygon.push_back({points.back().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)}); painter.polygon(polygon, Pen{.style = Line_Style::None}, brush); } painter.polyline(points, pen); } double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_Frame& frame, double frequency, bool& ok) { ok = false; if(frame.samples.empty() || !properties.frequency_range.contains(frequency) || properties.frequency_range.length() == 0.0) return 0.0; const double normalized = (frequency - properties.frequency_range.origin) / properties.frequency_range.length(); const double position = std::clamp(normalized, 0.0, 1.0) * static_cast(frame.samples.size() - 1); const auto lower = static_cast(std::floor(position)); const auto upper = std::min(lower + 1, frame.samples.size() - 1); const double fraction = position - static_cast(lower); ok = true; return frame.samples[lower] * (1.0 - fraction) + frame.samples[upper] * fraction; } } struct Spectrum_Control::Impl { Impl(Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) : frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(owner) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; Plottable_Latest_Real_Time_Data frame; Spectrum_Interaction_State interaction; std::mutex frame_update_mutex; }; Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(power_axis))) {} Spectrum_Control::~Spectrum_Control() = default; void Spectrum_Control::update_samples(std::span values) { if(get<&Spectrum_Properties::frequency_point_size>() <= 0) set<&Spectrum_Properties::frequency_point_size>(static_cast(values.size())); std::lock_guard lock(impl_->frame_update_mutex); Spectrum_Frame frame = impl_->frame.snapshot().value_or(Spectrum_Frame{}); frame.samples.assign(values.begin(), values.end()); if(frame.maxima.size() != values.size()) frame.maxima.assign(values.begin(), values.end()); else for(std::size_t index = 0; index < values.size(); ++index) frame.maxima[index] = std::max(frame.maxima[index], values[index]); if(frame.minima.size() != values.size()) frame.minima.assign(values.begin(), values.end()); else for(std::size_t index = 0; index < values.size(); ++index) frame.minima[index] = std::min(frame.minima[index], values[index]); impl_->frame.update(std::move(frame)); changed(); } void Spectrum_Control::update_samples(std::pmr::vector&& values) { update_samples(std::span(values.data(), values.size())); } std::size_t Spectrum_Control::sample_count() const { const auto frame = impl_->frame.snapshot(); return frame ? frame->samples.size() : 0; } std::size_t Spectrum_Control::rendered_point_count() const { const auto state = properties(); const auto frame = impl_->frame.snapshot(); return frame ? curve_points(frame->samples, state.frequency_range, impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.visible_range_only, state.interpolation_mode).size() : 0; } double Spectrum_Control::power_at(double frequency, bool& ok) const { const auto state = properties(); const auto frame = impl_->frame.snapshot(); return frame ? spectrum_power_at(state, *frame, frequency, ok) : (ok = false, 0.0); } void Spectrum_Control::add_custom_marker(double frequency) { add_custom_line_marker(frequency); } void Spectrum_Control::add_custom_line_marker(double frequency) { impl_->interaction.update([frequency](Spectrum_Interaction& interaction) { interaction.markers.push_back(frequency); }); changed(); } void Spectrum_Control::remove_custom_marker(double frequency) { bool removed{}; impl_->interaction.update([&](Spectrum_Interaction& interaction) { if(interaction.markers.empty()) return; auto closest = std::min_element(interaction.markers.begin(), interaction.markers.end(), [frequency](double left, double right) { return std::abs(left - frequency) < std::abs(right - frequency); }); const int removed_index = static_cast(std::distance(interaction.markers.begin(), closest)); interaction.markers.erase(closest); if(interaction.selected_marker == removed_index) interaction.selected_marker = -1; else if(interaction.selected_marker > removed_index) --interaction.selected_marker; removed = true; }); if(removed) changed(); } void Spectrum_Control::remove_selected_marker() { bool removed{}; impl_->interaction.update([&](Spectrum_Interaction& interaction) { if(interaction.selected_marker < 0 || interaction.selected_marker >= static_cast(interaction.markers.size())) return; interaction.markers.erase(interaction.markers.begin() + interaction.selected_marker); interaction.selected_marker = -1; removed = true; }); if(removed) changed(); } void Spectrum_Control::clear_custom_markers() { impl_->interaction.update([](Spectrum_Interaction& interaction) { interaction.markers.clear(); interaction.selected_marker = -1; }); changed(); } int Spectrum_Control::selectable_line_marker_count() const { return impl_->interaction.read([](const Spectrum_Interaction& interaction) { return static_cast(interaction.markers.size()); }); } int Spectrum_Control::selected_marker_index() const { return impl_->interaction.get<&Spectrum_Interaction::selected_marker>(); } void Spectrum_Control::set_selected_marker_index(int index) { impl_->interaction.update([index](Spectrum_Interaction& interaction) { interaction.selected_marker = index >= 0 && index < static_cast(interaction.markers.size()) ? index : -1; }); changed(); } void Spectrum_Control::select_next_marker() { impl_->interaction.update([](Spectrum_Interaction& interaction) { if(interaction.markers.empty()) interaction.selected_marker = -1; else interaction.selected_marker = (interaction.selected_marker + 1) % static_cast(interaction.markers.size()); }); changed(); } void Spectrum_Control::select_previous_marker() { impl_->interaction.update([](Spectrum_Interaction& interaction) { if(interaction.markers.empty()) interaction.selected_marker = -1; else interaction.selected_marker = (interaction.selected_marker <= 0 ? static_cast(interaction.markers.size()) : interaction.selected_marker) - 1; }); changed(); } void Spectrum_Control::clear_marker_selection() { set_selected_marker_index(-1); } double Spectrum_Control::marker_frequency(int index) const { return impl_->interaction.read([index](const Spectrum_Interaction& interaction) { return index >= 0 && index < static_cast(interaction.markers.size()) ? interaction.markers[index] : 0.0; }); } void Spectrum_Control::set_marker_frequency(int index, double frequency) { bool updated{}; impl_->interaction.update([&](Spectrum_Interaction& interaction) { if(index < 0 || index >= static_cast(interaction.markers.size())) return; interaction.markers[index] = frequency; updated = true; }); if(updated) changed(); } void Spectrum_Control::set_current_marker_frequency(double frequency) { const int index = selected_marker_index(); if(index >= 0) set_marker_frequency(index, frequency); } void Spectrum_Control::handle_event(const Event& event) { bool updated{}; impl_->interaction.update([&](Spectrum_Interaction& interaction) { updated = update_hover_tooltip(interaction.tooltip, event); }); if(updated) changed(); } void Spectrum_Control::publish() { publish_properties(); impl_->interaction.publish(); } void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { const auto& state = render_properties(view); const auto& published_frame = view.get(impl_->frame); const Spectrum_Frame empty_frame; const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame; const auto& interaction = view.get(impl_->interaction); const Axis_Transform horizontal = impl_->frequency_axis->transform(view); const Axis_Transform vertical = impl_->power_axis->transform(view); const RectF content = axes_rect(horizontal, vertical); if(state.sweep_region_visible) { const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin); const double last = horizontal.coord_to_pixel(state.sweep_frequency_range.target); painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_region_brush); } if(state.max_hold_visible) draw_curve(painter, frame.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush); if(state.min_hold_visible) draw_curve(painter, frame.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush); draw_curve(painter, frame.samples, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.current_pen, state.current_brush); if(state.middle_frequency_pen.enabled()) { const double x = horizontal.coord_to_pixel(state.center_frequency); painter.line({x, content.y}, {x, content.bottom()}, state.middle_frequency_pen); } for(std::size_t index = 0; index < interaction.markers.size(); ++index) { const double x = horizontal.coord_to_pixel(interaction.markers[index]); painter.line({x, content.y}, {x, content.bottom()}, static_cast(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen); } if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) { const auto draw_extreme = [&](bool maximum) { auto iterator = maximum ? std::max_element(frame.samples.begin(), frame.samples.end()) : std::min_element(frame.samples.begin(), frame.samples.end()); const std::size_t index = static_cast(std::distance(frame.samples.begin(), iterator)); const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0; const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator; const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)}; const Pen& pen = maximum ? state.max_pen : state.min_pen; painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid}); }; if(state.max_marker_visible) draw_extreme(true); if(state.use_min_marker) draw_extreme(false); } if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) { const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x); bool ok{}; const double power = spectrum_power_at(state, frame, frequency, ok); if(ok) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz " << power; const RectF box{interaction.tooltip.position.x + 8.0, interaction.tooltip.position.y + 8.0, 170.0, 24.0}; painter.rect(box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen); } } } } }