Files
Renderive/web_server/app/Web_Plot_Session.cpp
T
2026-08-11 16:10:02 +08:00

326 lines
14 KiB
C++

#include "Web_Plot_Session.h"
#include "Pixel_Frame.h"
#include "render_2D/export.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <mutex>
#include <type_traits>
#include <utility>
#include <vector>
namespace renderive::web {
namespace {
Color blend(Color first, Color second, double amount) {
const auto channel = [amount](std::uint8_t a, std::uint8_t b) {
return static_cast<std::uint8_t>(std::clamp(
std::lround(a + (b - a) * amount), 0L, 255L));
};
return {
channel(first.r, second.r), channel(first.g, second.g),
channel(first.b, second.b), channel(first.a, second.a)
};
}
Color_Map radio_color_map() {
constexpr Color stops[] = {
{3, 7, 18, 255}, {16, 52, 105, 255}, {23, 163, 184, 255},
{238, 210, 91, 255}, {239, 68, 68, 255}
};
std::vector<Pixel> colors;
colors.reserve(256);
for (int index = 0; index < 256; ++index) {
const double position = index / 255.0 * (std::size(stops) - 1);
const auto stop = static_cast<std::size_t>(std::floor(position));
const auto next = std::min(stop + 1, std::size(stops) - 1);
colors.push_back(premultiply(blend(stops[stop], stops[next], position - stop)));
}
return Color_Map(std::move(colors));
}
Time_Of_Day current_time_of_day() {
constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL;
const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
return {now % day_ms};
}
double gaussian(double x, double center, double width) {
const double normalized = (x - center) / width;
return std::exp(-0.5 * normalized * normalized);
}
} // namespace
struct Web_Plot_Session::Impl {
Plot_Core plot;
std::shared_ptr<Frequency_Axis> spectrum_frequency_axis;
std::shared_ptr<Axis> spectrum_power_axis;
std::shared_ptr<Frequency_Axis> waterfall_frequency_axis;
std::shared_ptr<Time_Axis> waterfall_time_axis;
std::shared_ptr<Spectrum> spectrum;
std::shared_ptr<Waterfall> waterfall;
std::shared_ptr<Selection_Rectangle_Overlay> selection;
Demo_Mode mode = Demo_Mode::Fm;
double gain_db = 8.0;
std::uint64_t frame_index{};
std::mutex mutex;
Impl() {
plot.init();
plot.set_background_color({3, 7, 18, 255});
plot.set_max_render_fps(30.0);
plot.set_viewport_size({960, 600});
const auto root = plot.root_renderable();
const auto data = plot.create_renderable_node(root, "Web_Data");
const auto axes = plot.create_renderable_node(root, "Web_Axes");
const auto overlay = plot.create_renderable_node(root, "Web_Overlay");
axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel);
constexpr Color axis_color{93, 116, 151, 255};
const Range initial_frequency{101'300'000.0, 103'700'000.0};
spectrum_frequency_axis =
Frequency_Axis::Builder(axes, Orientation::Horizontal)
.set_coord_range(initial_frequency)
.set_label_precision(2)
.set_tick_length(8)
.set_sub_tick_length(4)
.set_color(axis_color)
.set_use_wheel(true)
.set_use_drag(true)
.build();
spectrum_power_axis =
Axis::Builder(axes, Orientation::Vertical)
.set_coord_range({-20.0, -120.0})
.set_label_precision(0)
.set_tick_length(-8)
.set_sub_tick_length(-4)
.set_color(axis_color)
.set_unit_text("dBm")
.build();
waterfall_frequency_axis =
Frequency_Axis::Builder(axes, Orientation::Horizontal)
.set_coord_range(initial_frequency)
.set_label_precision(2)
.set_tick_length(8)
.set_sub_tick_length(4)
.set_color(axis_color)
.set_use_wheel(true)
.set_use_drag(true)
.build();
waterfall_time_axis =
Time_Axis::Builder(axes, Orientation::Vertical)
.set_visible_time_point_count(72)
.set_tick_label_spacing_px(34)
.set_time_format("mm:ss")
.set_tick_length(-8)
.set_sub_tick_length(-4)
.set_color(axis_color)
.build();
spectrum = Spectrum::Builder(data, spectrum_frequency_axis, spectrum_power_axis)
.set_frequency_range(initial_frequency)
.set_frequency_point_size(768)
.set_center_frequency(102'500'000.0)
.set_sweep_frequency_range({102'200'000.0, 102'800'000.0})
.set_max_marker_visible(true)
.set_sweep_region_visible(true)
.set_interpolation_mode(Line_Interpolation_Mode::Cubic_Value)
.build();
spectrum->set_current_pen({
Color{57, 224, 177, 255}, 2.0, Line_Style::Solid,
Line_Cap::Round, Line_Join::Round
});
spectrum->set_current_brush({Color{31, 174, 145, 32}, Brush_Style::Solid});
spectrum->set_max_pen({Color{250, 204, 21, 210}, 1.0});
spectrum->set_middle_frequency_pen({
Color{56, 189, 248, 220}, 1.0,
Line_Style::Dash
});
waterfall = Waterfall::Builder(data, waterfall_frequency_axis, waterfall_time_axis)
.set_frequency_range(initial_frequency)
.set_power_range({-120.0, -20.0})
.set_frequency_bin_count(768)
.set_interpolation_mode(Image_Interpolation_Mode::Bilinear)
.set_color_map(radio_color_map())
.build();
selection = Selection_Rectangle_Overlay::Builder(
overlay, spectrum_frequency_axis, spectrum_power_axis)
.set_rect_brush({Color{56, 189, 248, 36}, Brush_Style::Solid})
.set_border_pen({Color{125, 211, 252, 230}, 1.0, Line_Style::Dash})
.build();
apply_layout(plot.viewport_size());
plot.activate_view();
update_model();
(void)plot.render_frame(true);
}
~Impl() {
plot.deactivate_view();
}
void apply_layout(Size viewport) {
const int left = viewport.width < 620 ? 54 : 72;
const int right = viewport.width < 620 ? 44 : 70;
const int top = 16;
const int bottom = viewport.height < 480 ? 24 : 32;
const int gap = viewport.height < 480 ? 36 : 48;
const int content_width = std::max(1, viewport.width - left - right);
const int available_height = std::max(2, viewport.height - top - bottom - gap);
const int spectrum_height = std::max(1, available_height * 43 / 100);
const int waterfall_y = top + spectrum_height + gap;
const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom);
spectrum_frequency_axis->set_x(left);
spectrum_frequency_axis->set_y(top + spectrum_height);
spectrum_frequency_axis->set_pixel_length(static_cast<std::size_t>(content_width));
spectrum_power_axis->set_x(left);
spectrum_power_axis->set_y(top);
spectrum_power_axis->set_pixel_length(static_cast<std::size_t>(spectrum_height));
waterfall_frequency_axis->set_x(left);
waterfall_frequency_axis->set_y(waterfall_y + waterfall_height);
waterfall_frequency_axis->set_pixel_length(static_cast<std::size_t>(content_width));
waterfall_time_axis->set_x(left);
waterfall_time_axis->set_y(waterfall_y);
waterfall_time_axis->set_pixel_length(static_cast<std::size_t>(waterfall_height));
}
void update_model() {
constexpr int sample_count = 768;
const double time = static_cast<double>(frame_index) * 0.075;
std::vector<double> samples(sample_count);
for (int index = 0; index < sample_count; ++index) {
const double x = static_cast<double>(index) / (sample_count - 1);
const double noise = std::sin(index * 12.9898 + frame_index * 0.371) *
std::sin(index * 0.137 + frame_index * 0.071);
double signal{};
switch (mode) {
case Demo_Mode::Am:
signal = 66.0 * gaussian(x, 0.5, 0.008) +
39.0 * gaussian(x, 0.42, 0.018) +
39.0 * gaussian(x, 0.58, 0.018);
break;
case Demo_Mode::Fm: {
const double moving = 0.5 + std::sin(time) * 0.035;
signal = 58.0 * gaussian(x, moving, 0.055) +
25.0 * gaussian(x, moving - 0.11, 0.023) +
25.0 * gaussian(x, moving + 0.11, 0.023);
break;
}
case Demo_Mode::Usb:
signal = 61.0 * gaussian(x, 0.57, 0.045) +
28.0 * gaussian(x, 0.68, 0.025);
break;
case Demo_Mode::Lsb:
signal = 61.0 * gaussian(x, 0.43, 0.045) +
28.0 * gaussian(x, 0.32, 0.025);
break;
}
samples[index] = std::clamp(-110.0 + noise * 4.5 + signal + gain_db,
-120.0, -20.0);
}
spectrum->update_samples(samples);
if ((frame_index & 1U) == 0U)
waterfall->append_row(current_time_of_day(), samples);
++frame_index;
}
void set_center_frequency(double megahertz) {
const double center = megahertz * 1'000'000.0;
const double bandwidth = spectrum_frequency_axis->coord_range().size();
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
spectrum_frequency_axis->set_coord_range(range);
waterfall_frequency_axis->set_coord_range(range);
spectrum->set_frequency_range(range);
spectrum->set_center_frequency(center);
spectrum->set_sweep_frequency_range({
center - bandwidth * 0.125,
center + bandwidth * 0.125
});
waterfall->set_frequency_range(range);
}
void set_bandwidth(double kilohertz) {
const double center = spectrum->center_frequency();
const double bandwidth = kilohertz * 1000.0;
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
spectrum_frequency_axis->set_coord_range(range);
waterfall_frequency_axis->set_coord_range(range);
spectrum->set_frequency_range(range);
spectrum->set_sweep_frequency_range({
center - bandwidth * 0.125,
center + bandwidth * 0.125
});
waterfall->set_frequency_range(range);
}
std::optional<std::string> render_pixels() {
if (!plot.view_active())
return std::nullopt;
update_model();
if (!plot.render_frame(true))
return std::nullopt;
std::string pixels;
const Color background = plot.background_color();
plot.with_frame([&pixels, background](Image_View image) {
pixels = encode_pixel_frame(image, background);
});
return pixels.empty() ? std::nullopt : std::optional<std::string>(std::move(pixels));
}
std::optional<Web_Response> handle(const Web_Event& event) {
std::lock_guard lock(mutex);
return std::visit(
[this](const auto& value) -> std::optional<Web_Response> {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, Frame_Request>) {
auto pixels = render_pixels();
if (pixels)
return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)};
}
else if constexpr (std::is_same_v<T, Viewport_Resize>) {
const Size previous = plot.viewport_size();
plot.set_viewport_size(value.size);
apply_layout(value.size);
Resize_Event resized;
resized.old_size = previous;
resized.new_size = value.size;
plot.dispatch_event(resized);
}
else if constexpr (std::is_same_v<T, Event>) {
if (value.type == Event_Type::Show)
plot.activate_view();
else if (value.type == Event_Type::Hide)
plot.deactivate_view();
plot.dispatch_event(value);
}
else if constexpr (std::is_base_of_v<Event, T>) {
plot.dispatch_event(value);
}
else if constexpr (std::is_same_v<T, Set_Demo_Mode>) {
mode = value.mode;
plot.notify_model_dirty();
}
else if constexpr (std::is_same_v<T, Set_Center_Frequency>) {
set_center_frequency(value.megahertz);
}
else if constexpr (std::is_same_v<T, Set_Bandwidth>) {
set_bandwidth(value.kilohertz);
}
else if constexpr (std::is_same_v<T, Set_Gain>) {
gain_db = value.decibels;
plot.notify_model_dirty();
}
else if constexpr (std::is_same_v<T, Set_Max_Hold>) {
spectrum->set_max_hold_visible(value.enabled);
}
else if constexpr (std::is_same_v<T, Set_Smoothing>) {
spectrum->set_interpolation_mode(
value.enabled
? Line_Interpolation_Mode::Cubic_Value
: Line_Interpolation_Mode::Nearest_Sample);
waterfall->set_interpolation_mode(
value.enabled
? Image_Interpolation_Mode::Bilinear
: Image_Interpolation_Mode::Nearest);
}
else if constexpr (std::is_same_v<T, Clear_Selection>) {
selection->clear_selected_regions();
}
return std::nullopt;
},
event);
}
};
Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique<Impl>()) {}
Web_Plot_Session::~Web_Plot_Session() = default;
std::optional<Web_Response> Web_Plot_Session::handle(const Web_Event& event) {
return impl_->handle(event);
}
} // namespace renderive::web