commit 73aec484df4b906e0c4e965a828ae7db52e364a8 Author: parovoz Date: Thu Sep 12 23:12:19 2024 +0400 init diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d771ba6 --- /dev/null +++ b/CMakeLists.txt @@ -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 +) + diff --git a/run.sh.in b/run.sh.in new file mode 100644 index 0000000..13f729c --- /dev/null +++ b/run.sh.in @@ -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 diff --git a/src/MyApplication.cpp b/src/MyApplication.cpp new file mode 100644 index 0000000..1e2c18d --- /dev/null +++ b/src/MyApplication.cpp @@ -0,0 +1,405 @@ +#include "MyApplication.h" + +#include +#include +#include +#include + +class SinModel : public Chart::WAbstractChartModel +{ +public: + SinModel(vector& 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() + ).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& sinuses; +}; + + +class FourierModel : public Chart::WAbstractChartModel +{ +public: + FourierModel(shared_ptr dm) + : Chart::WAbstractChartModel{} + , discreteModel{dm} + { + dm->changed().connect([this](){ + data_.clear(); + data_.reserve(rowCount()); + + vector 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 discreteModel; + vector> data_; +}; + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +MyApplication::MyApplication(const WEnvironment& env) + : WApplication{env} +{ + setTheme(make_shared()); + styleSheet().addRule("html, body", "height: 100%"); + root()->addStyleClass("d-flex h-100"); + + auto left = root()->addNew(); + left->addStyleClass("d-flex flex-column h-100"); + + { // график + auto chartContainer = left->addNew(); + chart = chartContainer->addNew(); + //chart->setBackground(WColor(220, 220, 220)); + chart->setType(Chart::ChartType::Scatter); + + + roughModel = make_shared(sinuses, 100); + auto roughSeries = make_unique(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(sinuses, 10000); + auto seriesPtr = make_unique(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(sinuses, 0); + auto pointsDSPtr = make_unique(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(roughSeries_); + sliderWidget->resize(800, 80); + sliderWidget->setSelectionAreaPadding(40, Side::Left | Side::Right); + } + + auto controlsContainer = left->addNew(); + controlsContainer->addStyleClass("h-100 d-flex flex-column"); + controlsContainer->setOverflow(Overflow::Hidden); + + { // функции + auto form = controlsContainer->addNew(); + form->addStyleClass("d-flex flex-row align-items-center gap-1 p-3"); + + list = controlsContainer->addNew(); + list->addStyleClass("d-flex flex-column gap-1 p-2"); + list->setOverflow(Overflow::Auto); + + showAll = form->addNew(); + 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("a="); + auto a = form->addNew(); + form->addNew("w="); + auto w = form->addNew(); + form->addNew("f="); + auto f = form->addNew(); + + for ( auto i : {a,w,f} ) { + i->setWidth(100); + i->setRange(-1000, 1000); + } + + auto addBtn = form->addNew("+"); + + addBtn->clicked().connect([=,this](){ + addSin({a->value(), w->value(), f->value()}); + }); + } + + + auto right = root()->addNew(); + right->addStyleClass("d-flex flex-column h-100 gap-1"); + right->setOverflow(Overflow::Hidden, Orientation::Horizontal); + { // дискретизация + auto form = right->addNew(); + form->addStyleClass("d-flex flex-row gap-3"); + + auto show = form->addNew("дискретизация"); + auto freq = form->addNew(); + 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(format("{:.2f}", x)) + ->addStyleClass("border rounded-3 p-1"); + } + }); + } + + vals = right->addNew(); + vals->setOverflow(Overflow::Auto, Orientation::Horizontal); + vals->addStyleClass("d-flex gap-1 p-1 text-nowrap"); + + fourierChart = right->addNew(); + fourierChart->setPlotAreaPadding(100); + fourierChart->setType(Chart::ChartType::Scatter); + fourierModel = make_shared(discreteModel); + fourierChart->setModel(static_pointer_cast(fourierModel)); + + auto seriesPtr = make_unique(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(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()); + 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(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(); + { + el->addStyleClass("border rounded-3 p-3 d-flex justify-content-between align-items-center gap-3"); + + auto show = el->addNew(); + 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(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(WString{"{1} cos( {2}t + {3} )"}.arg(s.a).arg(s.w).arg(s.f)); + auto del = el->addNew("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 + ); +} diff --git a/src/MyApplication.h b/src/MyApplication.h new file mode 100644 index 0000000..2a53d3e --- /dev/null +++ b/src/MyApplication.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +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 roughModel; + shared_ptr detailedModel; + shared_ptr discreteModel; + Chart::WDataSeries* pointsDS; + + Chart::WCartesianChart* fourierChart; + shared_ptr fourierModel; + + + WContainerWidget* list; + WCheckBox* showAll; + size_t checkCount = 0; + + WContainerWidget* vals; + + vector sinuses; + + void addSin(Sin s); + void removeSin(int index); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5db3ed5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +#include + +#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(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; +} diff --git a/wt_config.xml b/wt_config.xml new file mode 100644 index 0000000..946dcc5 --- /dev/null +++ b/wt_config.xml @@ -0,0 +1,55 @@ + + + + + true + false + + + + + true + + + X-Forwarded-For + + + + 127.0.0.1/8 + ::1/128 + + + + + + + + + + + + +