333 lines
15 KiB
C++
333 lines
15 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{}
|
|
.set<&Spectrum::Properties::frequency_range>(initial_frequency)
|
|
.set<&Spectrum::Properties::frequency_point_size>(768)
|
|
.set<&Spectrum::Properties::center_frequency>(102'500'000.0)
|
|
.set<&Spectrum::Properties::sweep_frequency_range>(Range{102'200'000.0, 102'800'000.0})
|
|
.set<&Spectrum::Properties::max_marker_visible>(true)
|
|
.set<&Spectrum::Properties::sweep_region_visible>(true)
|
|
.set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value)
|
|
.build(data, spectrum_frequency_axis, spectrum_power_axis);
|
|
spectrum->set<&Spectrum::Properties::current_pen>(Pen{
|
|
Color{57, 224, 177, 255}, 2.0, Line_Style::Solid,
|
|
Line_Cap::Round, Line_Join::Round
|
|
});
|
|
spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{31, 174, 145, 32}, Brush_Style::Solid});
|
|
spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{250, 204, 21, 210}, 1.0});
|
|
spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{
|
|
Color{56, 189, 248, 220}, 1.0,
|
|
Line_Style::Dash
|
|
});
|
|
waterfall = Waterfall::Builder{}
|
|
.set<&Waterfall::Properties::frequency_range>(initial_frequency)
|
|
.set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0})
|
|
.set<&Waterfall::Properties::frequency_bin_count>(768)
|
|
.set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear)
|
|
.set<&Waterfall::Properties::color_map>(radio_color_map())
|
|
.build(data, waterfall_frequency_axis, waterfall_time_axis);
|
|
selection = Selection_Rectangle_Overlay::Builder{}
|
|
.set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid})
|
|
.set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash})
|
|
.build(overlay, spectrum_frequency_axis, spectrum_power_axis);
|
|
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->update([&](Axis_Properties& state) {
|
|
state.x = left;
|
|
state.y = top + spectrum_height;
|
|
state.pixel_length = static_cast<std::size_t>(content_width);
|
|
});
|
|
spectrum_power_axis->update([&](Axis_Properties& state) {
|
|
state.x = left;
|
|
state.y = top;
|
|
state.pixel_length = static_cast<std::size_t>(spectrum_height);
|
|
});
|
|
waterfall_frequency_axis->update([&](Axis_Properties& state) {
|
|
state.x = left;
|
|
state.y = waterfall_y + waterfall_height;
|
|
state.pixel_length = static_cast<std::size_t>(content_width);
|
|
});
|
|
waterfall_time_axis->update([&](auto& state) {
|
|
state.x = left;
|
|
state.y = waterfall_y;
|
|
state.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->get<&Axis_Properties::coordinates>().size();
|
|
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
|
|
spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range);
|
|
waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range);
|
|
spectrum->set<&Spectrum::Properties::frequency_range>(range);
|
|
spectrum->set<&Spectrum::Properties::center_frequency>(center);
|
|
spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{
|
|
center - bandwidth * 0.125,
|
|
center + bandwidth * 0.125
|
|
});
|
|
waterfall->set<&Waterfall::Properties::frequency_range>(range);
|
|
}
|
|
void set_bandwidth(double kilohertz) {
|
|
const double center = spectrum->get<&Spectrum::Properties::center_frequency>();
|
|
const double bandwidth = kilohertz * 1000.0;
|
|
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
|
|
spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range);
|
|
waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range);
|
|
spectrum->set<&Spectrum::Properties::frequency_range>(range);
|
|
spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{
|
|
center - bandwidth * 0.125,
|
|
center + bandwidth * 0.125
|
|
});
|
|
waterfall->set<&Waterfall::Properties::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<&Spectrum::Properties::max_hold_visible>(value.enabled);
|
|
}
|
|
else if constexpr (std::is_same_v<T, Set_Smoothing>) {
|
|
spectrum->set<&Spectrum::Properties::interpolation_mode>(
|
|
value.enabled
|
|
? Line_Interpolation_Mode::Cubic_Value
|
|
: Line_Interpolation_Mode::Nearest_Sample);
|
|
waterfall->set<&Waterfall::Properties::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
|