This commit is contained in:
parovoz 2024-09-12 23:12:19 +04:00 committed by Andrey Alekseev
commit 73aec484df
6 changed files with 621 additions and 0 deletions

48
CMakeLists.txt Normal file
View File

@ -0,0 +1,48 @@
cmake_minimum_required(VERSION 3.5)
project(fourier LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Wt REQUIRED
Wt
HTTP
)
include(GNUInstallDirs)
include_directories("src")
add_executable(${PROJECT_NAME}
src/main.cpp
src/MyApplication.h
src/MyApplication.cpp
)
target_link_libraries(${PROJECT_NAME}
Wt::Wt
Wt::HTTP
)
configure_file(
run.sh.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.wt
)
install( TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install( FILES wt_config.xml DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/${PROJECT_NAME})
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.wt
DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
)

25
run.sh.in Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
pid=''
run=true
start() {
@CMAKE_INSTALL_FULL_BINDIR@/@PROJECT_NAME@ \
--docroot=@CMAKE_INSTALL_FULL_DATADIR@/@PROJECT_NAME@/docroot \
--http-address 0.0.0.0 --http-port 8080 \
--config @CMAKE_INSTALL_FULL_SYSCONFDIR@/@PROJECT_NAME@/wt_config.xml \
--resources-dir /usr/share/Wt/resources &> >(logger --tag @PROJECT_NAME@) & pid="$!"
}
shutdown() {
run=false
(( $pid )) && kill "$pid"
}
trap shutdown EXIT
start
while $run
do
wait "$pid" || { logger --tag @PROJECT_NAME@ -p 3 "restarting server after error"; start; }
done

405
src/MyApplication.cpp Normal file
View File

@ -0,0 +1,405 @@
#include "MyApplication.h"
#include <Wt/Chart/WAbstractChartModel.h>
#include <cfloat>
#include <ranges>
#include <algorithm>
class SinModel : public Chart::WAbstractChartModel
{
public:
SinModel(vector<Sin>& data, int rows)
: Chart::WAbstractChartModel{}
, sinuses{data}
, rows_{rows}
{ }
virtual double data(int row, int column) const override {
double x = -M_PI + row * 2*M_PI / (rowCount() - 1);
switch ( column ) {
case 0: return x;
case 1:
return ranges::fold_left_first(
sinuses | views::transform([x](Sin s){return s.get(x);}),
std::plus<double>()
).value_or(0);
default:
try { return sinuses.at(column-2).get(x); }
catch (out_of_range e){ return 0; }
}
}
virtual int columnCount() const override { return 2 + sinuses.size(); };
virtual int rowCount() const override { return rows_; }
void setRows(int rows) {
rows_ = rows;
changed().emit();
}
private:
int rows_;
vector<Sin>& sinuses;
};
class FourierModel : public Chart::WAbstractChartModel
{
public:
FourierModel(shared_ptr<SinModel> dm)
: Chart::WAbstractChartModel{}
, discreteModel{dm}
{
dm->changed().connect([this](){
data_.clear();
data_.reserve(rowCount());
vector<double> vals;
vals.reserve( discreteModel->rowCount());
double first_positive = DBL_MAX;
for(int i = 0; i < discreteModel->rowCount(); i++) {
auto v = discreteModel->data(i, 1);
vals.push_back(v);
if ( abs(vals[i]) > 0 && abs(vals[i]) < first_positive )
first_positive = abs(vals[i]);
}
for ( int k = 0; k < rowCount(); k++ ) {
if ( first_positive > 0 ) {
double x = 0,
y = 0;
for ( int n = 0; n < discreteModel->rowCount(); n++ ) {
double e = 2*M_PI*k*n/discreteModel->rowCount();
double x = vals[n] / first_positive;
x+=x*cos(e);
y-=x*sin(e);
}
data_[k] = {sqrt(x*x+y*y), atan2(y,x)};
} else data_[k] = {0,0};
}
changed().emit();
});
}
virtual double data(int row, int column) const override {
//double x = row/(2*M_PI);
switch ( column ) {
case 0: return row;
case 1: return data_[row].first;
case 2: return data_[row].second;
default: return 0;
}
}
virtual int columnCount() const override { return 3; };
virtual int rowCount() const override { return discreteModel->rowCount()/2; }
private:
shared_ptr<SinModel> discreteModel;
vector<pair<double, double>> data_;
};
#include <Wt/WBootstrap5Theme.h>
#include <Wt/Chart/WAbstractChartModel.h>
#include <Wt/Chart/WAxisSliderWidget.h>
#include <Wt/Chart/WCartesianChart.h>
#include <Wt/Chart/WDataSeries.h>
#include <Wt/WApplication.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WObject.h>
#include <Wt/WShadow.h>
#include <Wt/WSpinBox.h>
#include <Wt/WDoubleSpinBox.h>
#include <Wt/WLabel.h>
#include <Wt/WPushButton.h>
#include <Wt/WColorPicker.h>
#include <Wt/WCheckBox.h>
#include <Wt/WStandardItemModel.h>
MyApplication::MyApplication(const WEnvironment& env)
: WApplication{env}
{
setTheme(make_shared<WBootstrap5Theme>());
styleSheet().addRule("html, body", "height: 100%");
root()->addStyleClass("d-flex h-100");
auto left = root()->addNew<WContainerWidget>();
left->addStyleClass("d-flex flex-column h-100");
{ // график
auto chartContainer = left->addNew<WContainerWidget>();
chart = chartContainer->addNew<Chart::WCartesianChart>();
//chart->setBackground(WColor(220, 220, 220));
chart->setType(Chart::ChartType::Scatter);
roughModel = make_shared<SinModel>(sinuses, 100);
auto roughSeries = make_unique<Chart::WDataSeries>(1, Chart::SeriesType::Line);
auto roughSeries_ = roughSeries.get();
roughSeries_->setModel(roughModel);
roughSeries_->setXSeriesColumn(0);
roughSeries->setHidden(true);
chart->addSeries(std::move(roughSeries));
detailedModel = make_shared<SinModel>(sinuses, 10000);
auto seriesPtr = make_unique<Chart::WDataSeries>(1, Chart::SeriesType::Line);
auto series = seriesPtr.get();
series->setModel(detailedModel);
series->setXSeriesColumn(0);
series->setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
chart->addSeries(std::move(seriesPtr));
chart->setFollowCurve(series);
discreteModel = make_shared<SinModel>(sinuses, 0);
auto pointsDSPtr = make_unique<Chart::WDataSeries>(1, Chart::SeriesType::Point);
pointsDS = pointsDSPtr.get();
pointsDS->setModel(discreteModel);
pointsDS->setXSeriesColumn(0);
pointsDS->setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
pointsDS->setHidden(true);
pointsDS->setLabelsEnabled(Chart::Axis::Y);
pointsDS->setLabelColor(StandardColor::Black);
chart->addSeries(std::move(pointsDSPtr));
chart->axis(Chart::Axis::X).setMaximumZoomRange( 2*M_PI+1 );
chart->axis(Chart::Axis::X).setMinimumZoomRange( M_PI / 16.0 );
chart->axis(Chart::Axis::X).setMinimum( -M_PI );
chart->axis(Chart::Axis::X).setMaximum( M_PI );
chart->axis(Chart::Axis::Y).setMinimumZoomRange(0.1);
chart->axis(Chart::Axis::X).setLocation(Chart::AxisValue::Zero);
chart->axis(Chart::Axis::Y).setLocation(Chart::AxisValue::Zero);
chart->axis(Chart::Axis::X).setGridLinesEnabled(true);
chart->axis(Chart::Axis::Y).setGridLinesEnabled(true);
chart->resize(800, 600);
chart->setPanEnabled(true);
chart->setZoomEnabled(true);
chart->setCrosshairEnabled(true);
chart->setOnDemandLoadingEnabled(true);
auto sliderWidget = chartContainer->addNew<Chart::WAxisSliderWidget>(roughSeries_);
sliderWidget->resize(800, 80);
sliderWidget->setSelectionAreaPadding(40, Side::Left | Side::Right);
}
auto controlsContainer = left->addNew<WContainerWidget>();
controlsContainer->addStyleClass("h-100 d-flex flex-column");
controlsContainer->setOverflow(Overflow::Hidden);
{ // функции
auto form = controlsContainer->addNew<WContainerWidget>();
form->addStyleClass("d-flex flex-row align-items-center gap-1 p-3");
list = controlsContainer->addNew<WContainerWidget>();
list->addStyleClass("d-flex flex-column gap-1 p-2");
list->setOverflow(Overflow::Auto);
showAll = form->addNew<WCheckBox>();
showAll->setTristate();
showAll->clicked().connect([=,this](){
for ( auto s : sinuses ) {
((WCheckBox*)s.el->children().at(0))
->setChecked(showAll->isChecked());
s.ds->setHidden(!showAll->isChecked());
}
checkCount = showAll->isChecked() ? sinuses.size() : 0;
chart->update();
});
form->addNew<WText>("a=");
auto a = form->addNew<WDoubleSpinBox>();
form->addNew<WText>("w=");
auto w = form->addNew<WDoubleSpinBox>();
form->addNew<WText>("f=");
auto f = form->addNew<WDoubleSpinBox>();
for ( auto i : {a,w,f} ) {
i->setWidth(100);
i->setRange(-1000, 1000);
}
auto addBtn = form->addNew<WPushButton>("+");
addBtn->clicked().connect([=,this](){
addSin({a->value(), w->value(), f->value()});
});
}
auto right = root()->addNew<WContainerWidget>();
right->addStyleClass("d-flex flex-column h-100 gap-1");
right->setOverflow(Overflow::Hidden, Orientation::Horizontal);
{ // дискретизация
auto form = right->addNew<WContainerWidget>();
form->addStyleClass("d-flex flex-row gap-3");
auto show = form->addNew<WCheckBox>("дискретизация");
auto freq = form->addNew<WSpinBox>();
freq->setWidth(120);
freq->setRange(0, 1000);
show->clicked().connect([=,this](){
pointsDS->setHidden(!show->isChecked());
chart->update();
});
freq->changed().connect([=,this](){
discreteModel->setRows(freq->value());
});
discreteModel->changed().connect([this](){
vals->clear();
for ( int i = 0; i < discreteModel->rowCount(); i++ ) {
auto x = discreteModel->data(i,1);
vals->addNew<WText>(format("{:.2f}", x))
->addStyleClass("border rounded-3 p-1");
}
});
}
vals = right->addNew<WContainerWidget>();
vals->setOverflow(Overflow::Auto, Orientation::Horizontal);
vals->addStyleClass("d-flex gap-1 p-1 text-nowrap");
fourierChart = right->addNew<Chart::WCartesianChart>();
fourierChart->setPlotAreaPadding(100);
fourierChart->setType(Chart::ChartType::Scatter);
fourierModel = make_shared<FourierModel>(discreteModel);
fourierChart->setModel(static_pointer_cast<Chart::WAbstractChartModel>(fourierModel));
auto seriesPtr = make_unique<Chart::WDataSeries>(2, Chart::SeriesType::Point);
auto phaseSeries = seriesPtr.get();
phaseSeries->setModel(fourierChart->model());
phaseSeries->setXSeriesColumn(0);
phaseSeries->setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
fourierChart->addSeries(std::move(seriesPtr));
seriesPtr = make_unique<Chart::WDataSeries>(1, Chart::SeriesType::Line);
auto ampSeries = seriesPtr.get();
ampSeries->setModel(fourierChart->model());
ampSeries->setXSeriesColumn(0);
ampSeries->setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
fourierChart->addSeries(std::move(seriesPtr));
auto id = fourierChart->addYAxis(make_unique<Chart::WAxis>());
ampSeries->bindToYAxis(id);
fourierChart->setFollowCurve(ampSeries);
fourierChart->yAxis(0).setTitle("φ");
fourierChart->yAxis(0).setTextPen(phaseSeries->pen());
fourierChart->yAxis(id).setTitle("α");
fourierChart->yAxis(id).setTextPen(ampSeries->pen());
fourierChart->axis(Chart::Axis::X).setLabelAngle(-90);
fourierChart->setPanEnabled(true);
fourierChart->setZoomEnabled(true);
fourierChart->setCrosshairEnabled(true);
fourierChart->resize(800, 600);
}
void MyApplication::addSin(Sin s) {
if ( find(sinuses.cbegin(), sinuses.cend(), s) != sinuses.cend() ) return;
sinuses.push_back(s);
int index = sinuses.size()-1;
roughModel->changed().emit();
detailedModel->changed().emit();
discreteModel->changed().emit();
auto seriesPtr = make_unique<Chart::WDataSeries>(index+2, Chart::SeriesType::Line);
auto series = seriesPtr.get();
series->setModel(detailedModel);
series->setXSeriesColumn(0);
series->setShadow({3, 3, WColor{0, 0, 0, 127}, 3});
series->setPen({StandardColor::Green});
chart->addSeries(std::move(seriesPtr));
sinuses[index].ds = series;
series->setHidden(true);
auto el = list->addNew<WContainerWidget>();
{
el->addStyleClass("border rounded-3 p-3 d-flex justify-content-between align-items-center gap-3");
auto show = el->addNew<WCheckBox>();
show->clicked().connect([=,this](){
sinuses.at(series->modelColumn()-2).ds->setHidden(!show->isChecked());
chart->update();
checkCount+= show->isChecked() ? 1 : -1;
showAll->setCheckState(
checkCount == 0 ? CheckState::Unchecked
: checkCount < sinuses.size() ? CheckState::PartiallyChecked
: CheckState::Checked
);
});
auto c = el->addNew<WColorPicker>(WColor{StandardColor::Black});
c->setColor(StandardColor::Green);
c->changed().connect([=,this](){
sinuses.at(series->modelColumn()-2).ds->setPen(WPen{c->color()});
chart->update();
});
c->setWidth(50);
el->addNew<WText>(WString{"{1} cos( {2}t + {3} )"}.arg(s.a).arg(s.w).arg(s.f));
auto del = el->addNew<WPushButton>("x");
del->setStyleClass("btn-danger");
del->clicked().connect([=,this](){
removeSin(series->modelColumn()-2);
el->removeFromParent();
});
}
sinuses[index].el = el;
int freq = 2*M_PI / ranges::max_element(sinuses, [](Sin a, Sin b){return abs(a.w) > abs(b.w);})->w * 4;
if ( detailedModel->rowCount() < freq ) detailedModel->setRows(freq);
else detailedModel->setRows(10000);
showAll->setCheckState(
checkCount == 0 ? CheckState::Unchecked
: checkCount < sinuses.size() ? CheckState::PartiallyChecked
: CheckState::Checked
);
}
void MyApplication::removeSin(int index) {
auto& s = sinuses.at(index);
chart->removeSeries(s.ds);
sinuses.erase(next(sinuses.begin(), index));
for ( ; index < sinuses.size(); index++ )
sinuses[index].ds->setModelColumn(index+2);
roughModel->changed().emit();
detailedModel->changed().emit();
discreteModel->changed().emit();
showAll->setCheckState(
checkCount == 0 ? CheckState::Unchecked
: checkCount < sinuses.size() ? CheckState::PartiallyChecked
: CheckState::Checked
);
}

60
src/MyApplication.h Normal file
View File

@ -0,0 +1,60 @@
#pragma once
#include <Wt/WApplication.h>
#include <cmath>
using namespace Wt;
using namespace std;
struct sin {
double a;
double w;
double f;
Chart::WDataSeries* ds;
WContainerWidget* el;
double get(double t) const
{ return a*cos(w*t+f); }
bool operator ==(const sin& other) const {
return a == other.a && w == other.w && f == other.f;
}
bool operator <(const sin& other) const {
return (a+w+f) < (other.a + other.w + other.f);
}
};
typedef struct sin Sin;
class SinModel;
class FourierModel;
class MyApplication : public WApplication
{
public:
MyApplication(const WEnvironment& env);
private:
Chart::WCartesianChart* chart;
shared_ptr<SinModel> roughModel;
shared_ptr<SinModel> detailedModel;
shared_ptr<SinModel> discreteModel;
Chart::WDataSeries* pointsDS;
Chart::WCartesianChart* fourierChart;
shared_ptr<FourierModel> fourierModel;
WContainerWidget* list;
WCheckBox* showAll;
size_t checkCount = 0;
WContainerWidget* vals;
vector<Sin> sinuses;
void addSin(Sin s);
void removeSin(int index);
};

28
src/main.cpp Normal file
View File

@ -0,0 +1,28 @@
#include <Wt/WServer.h>
#include "MyApplication.h"
using namespace std;
using namespace Wt;
int main(int argc, char *argv[]) {
int code = 0;
try {
WServer server(argc, argv, WTHTTP_CONFIGURATION);
using namespace placeholders;
server.addEntryPoint( EntryPointType::Application,
[](const WEnvironment& env){return make_unique<MyApplication>(env);},
"/"
);
server.run();
}
catch ( WServer::Exception& e ) { cerr << e.what() << endl; code = -1; }
catch ( std::exception& e ) { cerr << "exception: " << e.what() << endl; code = -1; }
return code;
}

55
wt_config.xml Normal file
View File

@ -0,0 +1,55 @@
<!--
Wt Configuration File.
The Wt configuration file manages, for every Wt application, settings
for session management, debugging, directory for runtime information
such as session sockets, and some security settings.
Settings may be specified globally, or for a single application path.
The path should be as configured in the Wt build process, where it
defaults to /etc/wt/wt_config.xml. It can be overridden in the environment
variable WT_CONFIG_XML, or with the -c startup option of wthttp.
The values listed here are the default values, which are used when the
declaration is missing or no configuration file is used.
-->
<server>
<application-settings location="*">
<web-sockets>true</web-sockets>
<progressive-bootstrap>false</progressive-bootstrap>
<head-matter user-agent=".*">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head-matter>
<behind-reverse-proxy>true</behind-reverse-proxy>
<trusted-proxy-config>
<original-ip-header>X-Forwarded-For</original-ip-header>
<trusted-proxies>
<!-- loopback -->
<proxy>127.0.0.1/8</proxy>
<proxy>::1/128</proxy>
<!-- link local -->
<!--
<proxy>169.254.0.0/16</proxy>
<proxy>fe80::/10</proxy>
-->
<!-- local -->
<!--
<proxy>10.0.0.0/8</proxy>
<proxy>172.16.0.0/12</proxy>
<proxy>192.168.0.0/16</proxy>
<proxy>fc00::/7</proxy>
-->
</trusted-proxies>
</trusted-proxy-config>
<properties>
</properties>
</application-settings>
</server>