#include #include #include #include "GWt_pspaApplication.h" #include "GWt_LigneFaisceau.h" #include "GWt_globalParameters.h" #include "GWt_dialog.h" #include "particleBeam.h" #include "bareParticle.h" #include "nomDeLogiciel.h" #include "mixedTools.h" #include "nomdElements.h" #include "environmentVariables.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Wt::Chart; /* * The env argument contains information about the new session, and * the initial request. It must be passed to the WApplication * constructor so it is typically also an argument for your custom * application constructor. */ PspaApplication::PspaApplication(const WEnvironment& env) : WApplication(env) { nameOfCase_ = "pspa"; // default setTitle("portail PSPA"); // application title if (!wApp->environment().javaScript()) { new WText("This examples requires that javascript support is enabled.",root()); } WContainerWidget *w = root(); w->setStyleClass("PSPA"); dtmanage_ = new dataManager(); /* * The main layout is a 3x2 grid layout. */ WGridLayout *layout = new WGridLayout(); layout->addWidget(createTitle("Menu (In future)"), 0, 0, 1, 2); WHBoxLayout *toolbarLayout = new WHBoxLayout(); WPushButton* boutonSauve = new WPushButton(" sauvegarder la config"); WPushButton* boutonRestaure = new WPushButton(" restaurer la config"); WPushButton* boutonLoadNew = new WPushButton(" charger une nouvelle config"); boutonSauve->setMinimumSize(30,30); boutonRestaure->setMinimumSize(30,30); // boutonLoadNew->setMinimumSize(30,30); // Upload when the button is clicked. // React to a succesfull upload. boutonLoadNew->clicked().connect(this, &PspaApplication::openFileSelector); toolbarLayout->addWidget(boutonSauve , 1); toolbarLayout->addWidget(boutonRestaure , 1); toolbarLayout->addWidget(boutonLoadNew , 1); boutonSauve->clicked().connect(this, &PspaApplication::sauver); boutonRestaure->clicked().connect(this, &PspaApplication::restaurer); layout->addLayout(toolbarLayout, 1, 0, 1, 2); layout->addWidget(createPalette(), 2, 0, 4, 1); beamLine_ = createBeamLine(); layout->addWidget(beamLine_, 2, 1, 1, 1); console_ = new WContainerWidget(); console_->decorationStyle().setBackgroundColor (WColor("lightgray")); console_->setMaximumSize(600,200); layout->addWidget(console_, 3, 1); console_->setMinimumSize(300,100); console_->setOverflow(WContainerWidget::OverflowAuto); //----------- // A supprimer et a mettre en fenetre globalParam_ = createGlobalParamWidget(); // leDessin_ = new WContainerWidget(); leDessin_ = createDrawingWidget(); WWidget* executeWidget = createExecuteWidget(); layout->addWidget( globalParam_, 4, 1); layout->addWidget( leDessin_, 3, 2); layout->addWidget( executeWidget , 5, 1); //----------- layout->setColumnResizable(1); layout->setRowResizable(2); /* * Let row 2 and column 1 take the excess space. */ layout->setRowStretch(2, 1); layout->setColumnStretch(1, 1); w->setLayout(layout); } WWidget* PspaApplication::createPalette() { WContainerWidget* palette=new WContainerWidget(); // palette->setLayout(new WVBoxLayout()); // nomdElements *e= new nomdElements(); // int nElts= e->getNumberOfElements(); // delete e; // nomdElements bidon; int nElts= nomdElements::getNumberOfElements(); for(int k = 0; k < nElts; k++) { typedElement eType= (typedElement)k; string image = nomdElements::getImageFromType(eType); createDragImage(image.c_str(), image.c_str(),image.c_str(), palette,nomdElements::getLabelFromType(eType)); new WBreak(palette); } // palette->setMinimumSize(100,300); return palette; } void PspaApplication::createDragImage(const char *url,const char *smallurl,const char *mimeType,WContainerWidget *p,WString name) { WImage *result = new WImage(url,p); WImage *dragImage = new WImage(smallurl,p); /* * Set the image to be draggable, showing the other image (dragImage) * to be used as the widget that is visually dragged. */ result->setDraggable(mimeType,dragImage,true); } WWidget* PspaApplication::createBeamLine() { WContainerWidget* beamLine = new GWt_LigneFaisceau(this); beamLine->setMinimumSize(300,100); return beamLine; } WWidget* PspaApplication::createGlobalParamWidget() { WContainerWidget* globalParam = new GWt_globalParameters(this); globalParam->setMaximumSize(600,150); globalParam->setMinimumSize(600,150); return globalParam; } WWidget* PspaApplication::createExecuteWidget() { WContainerWidget* executeW = new WContainerWidget(); executeW->setMaximumSize(600,150); executeW->setMinimumSize(600,150); // bouton execute exec_go_ = new WPushButton("execute!"); // exec_go_->setMinimumSize(300,300); exec_go_->setDisabled(true); exec_go_->clicked().connect(this, &PspaApplication::executer); // preparation du bouton add WPushButton* exec_add = new WPushButton("add"); exec_add->clicked().connect(this, &PspaApplication::addSectionToExecuteW); // preparation du bouton delete WPushButton* exec_delete = new WPushButton("delete"); exec_delete->clicked().connect(this, &PspaApplication::deleteSectionToExecuteW); // preparation du bouton push_ok WPushButton* exec_ok = new WPushButton("check/ok"); exec_ok->clicked().connect(this, &PspaApplication::checkSectionSelection); // le panel WPanel *panelLogiciels = new WPanel(executeW); panelLogiciels->setTitle(" sections of beam Line for executing softwares "); contenuSections_ = new WContainerWidget(); contenuSections_->addWidget(exec_add); contenuSections_->addWidget(exec_delete); contenuSections_->addWidget(exec_ok); contenuSections_->addWidget(exec_go_); contenuSections_->addWidget(new WBreak()); contenuSections_->addWidget(new WBreak()); addSectionToExecuteW(); panelLogiciels->setCentralWidget(contenuSections_); return executeW; } WContainerWidget* PspaApplication::createDrawingWidget() { WContainerWidget* dessin = new WContainerWidget(); dessin->addWidget(new WText(" graphic analysis : ")); dessin->addWidget(new WBreak()); dessin->addWidget(new WText(" drawing after element : ")); choixElementDessin_ = new WComboBox(); choixTypeDessinFaisceau_ = new WComboBox(); choixTypeDessinFaisceau_->addItem("courant_snyder"); choixTypeDessinFaisceau_->addItem("macroparticles"); dessin->addWidget(choixElementDessin_); dessin->addWidget(choixTypeDessinFaisceau_); WPushButton* caroule = new WPushButton("dessiner"); dessin->addWidget(caroule); caroule->clicked().connect(this, &PspaApplication::dessiner); dessin->addWidget(new WBreak()); WPushButton* okEnv = new WPushButton("draw enveloppe"); dessin->addWidget(new WText(" drawing enveloppe : ")); choixEnveloppeDessin_ = new WComboBox(); choixEnveloppeDessin_->addItem("x"); choixEnveloppeDessin_->addItem("y"); dessin->addWidget(okEnv); okEnv->clicked().connect(this, &PspaApplication::dessinerEnveloppe); toto_ = new WContainerWidget(); dessin->addWidget(toto_); return dessin; } void PspaApplication::addSectionToExecuteW() { disableSectionExecute(); string premierText, dernierText; if(selectedSections_.size() == 0) { premierText = dtmanage_->getLabelFromElementNumero(1); dernierText = dtmanage_->getLabelFromElementNumero(dtmanage_->beamLineSize()); } else { dernierText = selectedSections_.back()->fin->text().toUTF8(); int dernierNumero = dtmanage_->getCollection()->getNumeroFromLabel(dernierText); dernierNumero++; if ( dernierNumero <= dtmanage_->beamLineSize() ) { premierText = dtmanage_->getLabelFromElementNumero(dernierNumero); } else { premierText = dtmanage_->getLabelFromElementNumero(dtmanage_->beamLineSize()); } dernierText = premierText; } // cout << "PspaApplication::addSectionToExecute() : " << premierText << " à " << dernierText << endl; WContainerWidget* newSection = new WContainerWidget; selectedSections_.push_back(new GWt_sectionToExecute); selectedSections_.back()->debut = new WLineEdit(); selectedSections_.back()->debut->setDisabled(true); selectedSections_.back()->debut->setText(premierText); selectedSections_.back()->fin = new WLineEdit(); selectedSections_.back()->fin->changed().connect(this,&PspaApplication::disableSectionExecute); selectedSections_.back()->fin->setText(dernierText); selectedSections_.back()->selection = new WComboBox(); selectedSections_.back()->ligneDeWidget = newSection; newSection->addWidget(new WText(" from : ")); newSection->addWidget(selectedSections_.back()->debut); newSection->addWidget(new WText(" to : ")); newSection->addWidget(selectedSections_.back()->fin); newSection->addWidget(selectedSections_.back()->selection); contenuSections_->addWidget(newSection); unsigned nb = nomDeLogiciel::getNumberOfSoftwares(); unsigned k; for(k = 0; k < nb; k++) { selectedSections_.back()->selection->addItem(nomDeLogiciel(k).getString()); } } void PspaApplication::disableSectionExecute() { exec_go_->setDisabled(true); } void PspaApplication::checkSectionSelection() { if ( selectedSections_.empty() ) return; // traitement de la premiere ligne // on impose le depart du calcul au premier element string premier = dtmanage_->getLabelFromElementNumero(1); (*selectedSections_.begin())->debut->setText(premier); string currentString = (*selectedSections_.begin())->fin->text().toUTF8(); int current = dtmanage_->getCollection()->getNumeroFromLabel( currentString); cout << " numero " << current << endl; // si la fin est mal definie on prend toute la config par defaut if ( current <= 0 || current > dtmanage_->beamLineSize() ) { current = dtmanage_->beamLineSize(); currentString = dtmanage_->getLabelFromElementNumero(current); (*selectedSections_.begin())->fin->setText(currentString); } current++; currentString = dtmanage_->getLabelFromElementNumero(current); // traitement des suivantes (on avance d'un cran dans la liste) list::iterator itr, itr0; itr0 = selectedSections_.begin(); itr0++; for (itr = itr0; itr != selectedSections_.end(); itr++) { // debut if ( current >= dtmanage_->beamLineSize() ) { addConsoleMessage(" bad section definition ! \n "); GWt_dialog warningDialog("PSPA : Vérification des sections", " bad section definition !", GWt_dialog::Error,true,true); warningDialog.exec(); return; } (*itr)->debut->setText(currentString); // fin string finString = (*itr)->fin->text().toUTF8(); int numeroFin = dtmanage_->getCollection()->getNumeroFromLabel( finString); if ( numeroFin <= current || numeroFin > dtmanage_->beamLineSize()) { addConsoleMessage(" bad section definition ! \n "); GWt_dialog warningDialog("PSPA : Vérification des sections", " bad section definition !", GWt_dialog::Error, true,true); warningDialog.exec(); return; } // preparation de la ligne suivante current = numeroFin +1; currentString = dtmanage_->getLabelFromElementNumero(current); } if (!areDataCoherent()) { GWt_dialog warningDialog("PSPA : Vérification des sections", " donnees incoherentes !", GWt_dialog::Error,true,true); warningDialog.exec(); } else { exec_go_->setDisabled(false); } } bool PspaApplication::areDataCoherent() { bool caMarche = true; dtmanage_->initializeExecution(); list::iterator itr; for(itr = selectedSections_.begin(); itr != selectedSections_.end(); itr++) { string debString = (*itr)->debut->text().toUTF8(); string finString = (*itr)->fin->text().toUTF8(); int debut = dtmanage_->getCollection()->getNumeroFromLabel(debString); int fin = dtmanage_->getCollection()->getNumeroFromLabel(finString); nomDeLogiciel prog = nomDeLogiciel ( (*itr)->selection->currentIndex() ); dtmanage_->addSectionToExecute(debut,fin,prog); } string diagnostic = dtmanage_->checkExecute(); if ( !diagnostic.empty() ) { caMarche = false; addConsoleMessage(diagnostic.c_str()); GWt_dialog calculDialog("PSPA : Erreur lors de check execute", diagnostic , GWt_dialog::Error,true,true); calculDialog.exec(); } return caMarche; } void PspaApplication::deleteSectionToExecuteW() { if ( selectedSections_.empty() ) return; disableSectionExecute(); selectedSections_.back()->ligneDeWidget->clear(); delete selectedSections_.back()->ligneDeWidget; selectedSections_.pop_back(); } void PspaApplication::executer() { addConsoleMessage(string("on va peut etre y arriver")); static_cast(globalParam_)->updateGlobals(); GWt_dialog calculDialog("Calcul en cours", "Veuillez patienter...", GWt_dialog::Wait, true,false); calculDialog.show(); processEvents(); string resultat; if ( !dtmanage_->executeAll(resultat)) { GWt_dialog warningDialog("PSPA : Echec", " echec lors de l'exécution !", GWt_dialog::Error, true,true); warningDialog.exec(); } // cout << " PspaApplication : retour d'execution resultat = " << resultat << endl; addConsoleMessage(resultat); // cout << " PspaApplication : affichage console termine " << endl; exec_go_->setDisabled(true); calculDialog.hide(); faireDessin(); } void PspaApplication::sauver() { cout << " on sauve " << endl; addConsoleMessage("sauvegarde"); dialogSave_ = new WDialog("save"); new WText("name of case : ",dialogSave_->contents()); saveNameEdit_ = new WLineEdit(nameOfCase_.c_str(), dialogSave_->contents()); WPushButton *annule = new WPushButton("cancel",dialogSave_->contents()); WPushButton *submit = new WPushButton("OK",dialogSave_->contents()); annule->clicked().connect(dialogSave_, &Wt::WDialog::reject); submit->clicked().connect(dialogSave_, &Wt::WDialog::accept); dialogSave_->finished().connect(this, &PspaApplication::dialogSaveDone); dialogSave_->show(); } void PspaApplication::dialogSaveDone(WDialog::DialogCode code) { if ( code != Wt::WDialog::Accepted ) { addConsoleMessage(" pas de sauvegarde"); return;} nameOfCase_ = saveNameEdit_->text().toUTF8(); cout << " PspaApplication::dialogSaveDone() nameOfCase_= " << nameOfCase_ << endl; delete dialogSave_; dialogSave_ = NULL; GWt_globalParameters* bibi = static_cast(globalParam_); bibi->updateGlobals(); dtmanage_->saveConfiguration(nameOfCase_); } void PspaApplication::restaurer() { addConsoleMessage(string("restauration...")); dtmanage_->restoreElements(WORKINGAREA + "pspa.save"); GWt_globalParameters* bibi = static_cast(globalParam_); bibi->renew(); GWt_LigneFaisceau* bobo = static_cast(beamLine_); bobo->restoreElementCollection(); addConsoleMessage(string("...terminee")); } void PspaApplication::openFileSelector() { WContainerWidget *result = new WContainerWidget(); WVBoxLayout* myLayout = new WVBoxLayout(); uploadFileSelectorWidget_ = new WFileUpload(); uploadFileSelectorWidget_->setFileTextSize(40); myLayout->addWidget(new WText("Select the configuration file for pspa : ")); myLayout->addWidget(uploadFileSelectorWidget_); result->setLayout (myLayout); // Upload automatically when the user entered a file. uploadFileSelectorWidget_->changed().connect(uploadFileSelectorWidget_, &WFileUpload::upload); // React to a succesfull upload. uploadFileSelectorWidget_->uploaded().connect(this, &PspaApplication::chargerConfig); // React to a fileupload problem. uploadFileSelectorWidget_->fileTooLarge().connect(this, &PspaApplication::fileTooLarge); GWt_dialog* fileSelectorDialog = new GWt_dialog("Load a file",result,false); fileSelectorDialog->exec(); } void PspaApplication::chargerConfig() { GWt_dialog* message= new GWt_dialog("File successfully upload","The file has been correctly upload to" + uploadFileSelectorWidget_->spoolFileName(),GWt_dialog::Warning,false,true); string nomDuFichier = (uploadFileSelectorWidget_->clientFileName()).toUTF8(); cout << " fichier client : " << nomDuFichier << endl; bool test = removeExtensionFromConfigName(nomDuFichier); cout << " fichier client sans extension : " << nomDuFichier << endl; if ( test ) { nameOfCase_ = nomDuFichier; addConsoleMessage(string("restauration...")); dtmanage_->restoreElements(uploadFileSelectorWidget_->spoolFileName()); GWt_globalParameters* bibi = static_cast(globalParam_); bibi->renew(); GWt_LigneFaisceau* bobo = static_cast(beamLine_); bobo->restoreElementCollection(); addConsoleMessage(string("...terminee")); message->show(); } } void PspaApplication::fileTooLarge() { std::stringstream stream; stream << maximumRequestSize (); std::string maxRequestSize(stream.str()); std::string message = "This file is too large, please select a one\n"; message += "Maximum file size is "+ maxRequestSize; message += " bytes\n"; GWt_dialog* messageBox= new GWt_dialog("Error during upload file" ,message ,GWt_dialog::Error,false,true); messageBox->show(); } void PspaApplication::faireDessin() { choixElementDessin_->clear(); int nombre = dtmanage_->beamLineSize(); for ( int numero =1; numero <= nombre; numero++) { choixElementDessin_->addItem(dtmanage_->getLabelFromElementNumero(numero)); } } void PspaApplication::dessiner() { toto_->clear(); int typeFaisceau = choixTypeDessinFaisceau_->currentIndex(); int index = choixElementDessin_->currentIndex(); particleBeam* beam = dtmanage_->getDiagnosticBeam(index); if ( typeFaisceau == 0 ) { if ( !beam->momentRepresentationOk() ) beam->buildMomentRepresentation(); faireDessinTransport(toto_, beam); } else if ( typeFaisceau == 1 ) { if ( beam->particleRepresentationOk() ) faireDessinParmela(toto_, beam); else { addConsoleMessage("the beam state does not allow providing a drawing"); GWt_dialog warningBeamState(" graphical analysis", "the beam state does not allow providing a drawing with macroparticles !", GWt_dialog::Error, false,true); warningBeamState.exec(); } } else { addConsoleMessage("type of drawing not programmed"); GWt_dialog warningTypeDrawing(" graphical analysis", "type of drawing not programmed !", GWt_dialog::Error, false,true); warningTypeDrawing.exec(); } ////////////////////////////////////////// } void PspaApplication::dessinerEnveloppe() { toto_->clear(); int typeEnveloppe = choixEnveloppeDessin_->currentIndex(); if ( typeEnveloppe == 0 ) { faireDessinEnveloppe(toto_, "x"); } else { addConsoleMessage("type of enveloppe drawing not programmed"); GWt_dialog warningTypeEnveloppe(" graphical analysis", "type of enveloppe drawing not programmed !", GWt_dialog::Error, false,true); warningTypeEnveloppe.exec(); } ////////////////////////////////////////// } void PspaApplication::faireDessinEnveloppe(WContainerWidget* toto, string type) { new WText("enveloppe", toto); unsigned nbel = dtmanage_->beamLineSize(); vector xcor; vector ycor; dtmanage_->donneesRmsEnveloppe(type,1, nbel, xcor,ycor); scatterPlot1D(toto,xcor,ycor); } void PspaApplication::faireDessinParmela(WContainerWidget* toto, particleBeam* beam) { // WContainerWidget* toto = static_cast(leDessin_); // // toto->clear(); // new WText("emittance", toto); // vector& partic = dtmanage_->getCurrentBeam()->getParticleVector(); new WText("emittance parmela", toto); vector& partic = beam->getParticleVector(); WStandardItemModel *model = new WStandardItemModel(partic.size(), 3, toto); // model->setHeaderData(0, WString("X")); // model->setHeaderData(1, WString("Y = sin(X)")); for (unsigned i = 0; i < partic.size(); ++i) { double x= partic.at(i).getPosition().getComponent(0); double begamz = partic.at(i).getBetaGamma().getComponent(2); double xp = partic.at(i).getBetaGamma().getComponent(0)/begamz; // cout << "x = " << x << " xp= " << xp << endl; model->setData(i, 0, 10*x); model->setData(i, 1,1.e3*xp); model->setData(i, 2,2.e3*xp); } WCartesianChart *chart = new WCartesianChart(toto); chart->setModel(model); // set the model chart->setXSeriesColumn(0); // set the column that holds the X data chart->setLegendEnabled(true); // enable the legend chart->setType(ScatterPlot); // set type to ScatterPlot // Typically, for mathematical functions, you want the axes to cross // at the 0 mark: chart->axis(XAxis).setLocation(ZeroValue); chart->axis(YAxis).setLocation(ZeroValue); // Provide space for the X and Y axis and title. chart->setPlotAreaPadding(80, Left); chart->setPlotAreaPadding(40, Top | Bottom); // Add the curves WDataSeries s(1, PointSeries, Y1Axis); // s.setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3)); // s.setMarker(SquareMarker); // s.setMarkerSize(600.); // cout << "le marker est : " << s.marker() << endl; chart->addSeries(s); chart->resize(300, 300); // WPaintedWidget must be given explicit size // chart->setMargin(10, Top | Bottom); // add margin vertically // chart->setMargin(WLength::Auto, Left | Right); // center horizontally } // void PspaApplication::addElemToGlobals() // { // static_cast(globalParam_)->addElem(); // } void PspaApplication::faireDessinTransport(WContainerWidget* toto, particleBeam* beam) { // WContainerWidget* toto = static_cast(leDessin_); // toto->clear(); new WText("emittance transport", toto); vector xcor; vector ycor; // dtmanage_->getCurrentBeam()->donneesDessinEllipseXxp(xcor,ycor); beam->donneesDessinEllipseXxp(xcor,ycor); scatterPlot1D(toto,xcor,ycor); } void PspaApplication::scatterPlot1D(WContainerWidget* toto, vector& xcor, vector& ycor) { int nbpts = xcor.size(); cout << " PspaApplication::scatterPlot1D nbpts = " << nbpts << endl; WStandardItemModel *model = new WStandardItemModel(nbpts, 2, toto); for (int i = 0; i < nbpts; ++i) { model->setData(i, 0, xcor.at(i)); model->setData(i, 1, ycor.at(i)); // cout << " PspaApplication::scatterPlot1D el= " << i+1 << " x= " << xcor.at(i) << " y= " << ycor.at(i) << endl; } WCartesianChart *chart = new WCartesianChart(toto); chart->setModel(model); // set the model chart->setXSeriesColumn(0); // set the column that holds the X data chart->setLegendEnabled(true); // enable the legend chart->setType(ScatterPlot); // set type to ScatterPlot // Typically, for mathematical functions, you want the axes to cross // at the 0 mark: chart->axis(XAxis).setLocation(ZeroValue); chart->axis(YAxis).setLocation(ZeroValue); // Provide space for the X and Y axis and title. chart->setPlotAreaPadding(80, Left); chart->setPlotAreaPadding(40, Top | Bottom); // Add the curves WDataSeries s(1, CurveSeries, Y1Axis); chart->addSeries(s); chart->resize(300, 300); // WPaintedWidget must be given explicit size } void PspaApplication::updateSelections() { string premierText, dernierText; if ( selectedSections_.size() > 0 ) { premierText = dtmanage_->getLabelFromElementNumero(1); dernierText = dtmanage_->getLabelFromElementNumero( dtmanage_->beamLineSize() ); (*selectedSections_.begin())->debut->setText(premierText); (*selectedSections_.begin())->fin->setText(dernierText); } cout << "PspaApplication::updateSelections(): " << premierText << " à " << dernierText << endl; } string PspaApplication::getSelection() { list::iterator itr = selectedSections_.begin(); string str = (*itr)->fin->text().toUTF8(); return str; } WText* PspaApplication::createTitle(const WString& title) { WText *result = new WText(title); result->setInline(false); result->setStyleClass("title"); result->setMinimumSize(30,30); return result; } void PspaApplication::addConsoleMessage(WString msg) { WText *w = new WText(console_); w->setText(msg); w->setInline(false); /* * Little javascript trick to make sure we scroll along with new content */ WApplication *app = WApplication::instance(); app->doJavaScript(console_->jsRef() + ".scrollTop += " + console_->jsRef() + ".scrollHeight;"); } bool PspaApplication::removeExtensionFromConfigName(string& config) { // string configName; string extension(".save"); bool test = true; string::size_type nn = config.rfind('.'); if ( nn == string::npos ) { // pas de point test = false; } string fin = config.substr(nn); if ( fin != extension ) { // l'extension n'est pas la bonne test = false; } if ( test ) { string::size_type count = config.length() - extension.length(); config = config.substr(0, count); } else { GWt_dialog checkConfigNameDialog(" checking config file name", " the file must have the extension " + extension, GWt_dialog::Error,true,true); checkConfigNameDialog.exec(); } return test; }