musrview: rewrite on top of the ROOT7 web graphics
musrview is now a client/server application following the ROOT7 architecture
(S. Linev et al., J. Phys. Conf. Ser. 1525 (2020) 012092): the C++ side holds
the data, an ROOT::RWebWindow ships it as JSON to a JSROOT page which renders
it. This addresses three long standing problems.
The fit parameter, theory and run info blocks were TPaveText/TLegend objects
which scaled their content down until it became unreadable as soon as a fit had
many parameters. They are HTML panels now: fixed font size, own scrollbar, and
each of them can be shown and hidden individually. The run info keeps listing
every run with the very symbol it is plotted with.
The command keys ('d', 'f', 'a', ...) were executed even while a text or latex
label was being typed into the canvas. The page checks whether a text input has
the focus, so typing a 'd' inserts a 'd'.
The plot views were encoded in a set of entangled variables plus a static one
which leaked state between canvases. PMusrViewMode describes them as four
orthogonal properties - domain, Fourier component, difference including whether
it is formed before or after the transform, and average - so that combinations
such as 'Fourier-difference-average' are well defined. PMusrViewModel::GetCurves()
is the single place which maps a view mode onto histograms; plotting and ascii
export both go through it and can no longer disagree, which removed some 700
lines of duplicated view logic from the export.
New classes, namespace musrview:
PMusrViewMode the decoupled view state, free of ROOT and GUI, unit tested
PMusrViewModel the GUI-free data layer, physics taken over unchanged
PMusrWebCanvas the web front end and the graphics export
Further notes:
- The interactive path builds no TCanvas at all, the spectra travel as a TList
of drawables. A TCanvas creates and maps its window already in the
constructor, which would put an empty ROOT canvas next to the musrview window.
- The graphics export keeps the classic pad layout and its 800x600 size and
runs with ROOT in batch mode, so batch operation needs no browser.
- The window opens at 1200x900, since toolbar and panels take their share.
- Non-muSR axis labels are taken from the data file again, and ROOT/TLatex
markup is rendered in the HTML panels as well.
- musrview needs a ROOT built with -Droot7=ON -Dwebgui=ON, plus -Dqt6web=ON for
the embedded window (--embedded, the default). --browser uses the standard
browser of the system, --browser=server only prints the URL.
- PMusrCanvas is no longer part of the build, the files are kept for reference.
Tests: 87/87 pass, among them a new unit test of the view modes and a new
musrview test covering the non-muSR fit type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6YiUNBTMDX9uA3Ts1w2nW
This commit is contained in:
1 parent
a95318dc9f
commit
3c043af4db
18 files changed
+7460
-148
No files matched your search
+217
-129
@@ -31,33 +31,35 @@
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
#include <TApplication.h>
|
||||
#include <TSAXParser.h>
|
||||
#include <TROOT.h>
|
||||
#include <TSAXParser.h>
|
||||
#include <TSystem.h>
|
||||
|
||||
#include <ROOT/RWebWindowsManager.hxx>
|
||||
|
||||
#ifdef HAVE_GIT_REV_H
|
||||
#include "git-revision.h"
|
||||
#endif
|
||||
|
||||
#include "PMusr.h"
|
||||
#include "PStartupHandler.h"
|
||||
#include "PMsgBox.h"
|
||||
#include "PMsrHandler.h"
|
||||
#include "PMusrWebCanvas.h"
|
||||
#include "PRunDataHandler.h"
|
||||
#include "PRunListCollection.h"
|
||||
#include "PMusrCanvas.h"
|
||||
#include "PMsgBox.h"
|
||||
|
||||
#include "PStartupHandler.h"
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
@@ -74,8 +76,16 @@ void musrview_syntax()
|
||||
std::cout << std::endl << " -f, --fourier: will directly present the Fourier transform of the <msr-file>.";
|
||||
std::cout << std::endl << " -a, --avg: will directly present the averaged data/Fourier of the <msr-file>.";
|
||||
std::cout << std::endl << " -1, --one_to_one: calculate theory points only at data points.";
|
||||
std::cout << std::endl << " display options (musrview is web based, see the musrfit documentation):";
|
||||
std::cout << std::endl << " --embedded : show the plots in an embedded browser window, i.e. musrview";
|
||||
std::cout << std::endl << " behaves like a stand-alone application. This is the default";
|
||||
std::cout << std::endl << " as long as ROOT provides the Qt6 web display.";
|
||||
std::cout << std::endl << " --browser : show the plots in the standard web browser of the system.";
|
||||
std::cout << std::endl << " --browser=<kind> : use a specific display, e.g. chrome, firefox, qt6, server.";
|
||||
std::cout << std::endl << " 'server' only prints the URL and starts no browser at all,";
|
||||
std::cout << std::endl << " which is what to use when musrfit runs on a remote machine.";
|
||||
std::cout << std::endl << " --<graphic-format-extension>: ";
|
||||
std::cout << std::endl << " will produce a graphics-output-file without starting a root session.";
|
||||
std::cout << std::endl << " will produce a graphics-output-file without opening a window.";
|
||||
std::cout << std::endl << " the name is based on the <msr-file>, e.g. 3310.msr -> 3310_0.png";
|
||||
std::cout << std::endl << " supported graphic-format-extension:";
|
||||
std::cout << std::endl << " eps, pdf, gif, jpg, png, svg, xpm, root";
|
||||
@@ -91,24 +101,64 @@ void musrview_syntax()
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Shows an error message in a message box.
|
||||
*
|
||||
* \param errMsg the message to be shown
|
||||
*/
|
||||
void musrview_error_msg(std::string errMsg)
|
||||
{
|
||||
int argc=0;
|
||||
char **argv;
|
||||
int argc=0;
|
||||
char **argv=nullptr;
|
||||
TApplication app("musrviewErrorMsg", &argc, argv);
|
||||
new PMsgBox(errMsg, gClient->GetRoot(), 600, 200);
|
||||
app.Run();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Decides which web display musrview should use.
|
||||
*
|
||||
* <p>Unless the user asked for something specific, the embedded Qt6 WebEngine
|
||||
* window is preferred, since it makes musrview appear as the stand-alone
|
||||
* application it has always been. If ROOT was built without the Qt6 web
|
||||
* display, the standard browser of the system is used instead.
|
||||
*
|
||||
* \param requested what the user asked for, empty if nothing was requested
|
||||
*
|
||||
* \return the display string to be handed to ROOT::RWebWindow::Show()
|
||||
*/
|
||||
std::string musrview_select_display(const std::string &requested)
|
||||
{
|
||||
if (!requested.empty())
|
||||
return requested;
|
||||
|
||||
if (const char *env = gSystem->Getenv("MUSRVIEW_BROWSER"))
|
||||
if (strlen(env) > 0)
|
||||
return std::string(env);
|
||||
|
||||
// prefer the embedded window if ROOT provides it
|
||||
gSystem->RedirectOutput("/dev/null", "a");
|
||||
const Int_t loaded = gSystem->Load("libROOTQt6WebDisplay");
|
||||
gSystem->RedirectOutput(nullptr);
|
||||
|
||||
if (loaded >= 0)
|
||||
return "qt6";
|
||||
|
||||
std::cout << std::endl << ">> musrview **INFO** ROOT has no Qt6 web display, using the standard browser.";
|
||||
std::cout << std::endl << ">> Rebuild ROOT with -Dqt6web=ON for an embedded window." << std::endl;
|
||||
|
||||
return std::string();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>The musrview program is used to show muSR fit results in graphical form.
|
||||
* From it also Fourier transforms, difference between data an theory can be formed.
|
||||
* For a detailed description/usage of the program, please see
|
||||
* \htmlonly <a href="http://lmu.web.psi.ch/musrfit/user/html/user-manual.html#musrview">musrview online help</a>
|
||||
* \endhtmlonly
|
||||
* \latexonly musrview online help: \texttt{http://lmu.web.psi.ch/musrfit/user/html/user-manual.html#musrview}
|
||||
* \endlatexonly
|
||||
*
|
||||
* <p>Since version 1.13 musrview is based on the ROOT7 web graphics: the plots
|
||||
* are rendered by JSROOT either in an embedded browser window or in the
|
||||
* standard browser of the system, while the C++ side only holds the data.
|
||||
*
|
||||
* <b>return:</b>
|
||||
* - PMUSR_SUCCESS if everthing went smooth
|
||||
@@ -131,6 +181,7 @@ int main(int argc, char *argv[])
|
||||
bool graphicsOutput{false};
|
||||
bool asciiOutput{false};
|
||||
std::string graphicsExtension{};
|
||||
std::string display{};
|
||||
int timeout{0};
|
||||
bool show_errMsgBox{false};
|
||||
std::stringstream errMsg;
|
||||
@@ -182,6 +233,16 @@ int main(int argc, char *argv[])
|
||||
avg = true;
|
||||
} else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "--one_to_one")) {
|
||||
theoAtData = true;
|
||||
} else if (!strcmp(argv[i], "--embedded")) {
|
||||
display = "qt6";
|
||||
} else if (!strcmp(argv[i], "--browser")) {
|
||||
display = "default";
|
||||
} else if (!strncmp(argv[i], "--browser=", 10)) {
|
||||
display = argv[i]+10;
|
||||
if (display.empty()) {
|
||||
show_syntax = true;
|
||||
break;
|
||||
}
|
||||
} else if (!strcmp(argv[i], "--eps") || !strcmp(argv[i], "--pdf") || !strcmp(argv[i], "--gif") ||
|
||||
!strcmp(argv[i], "--jpg") || !strcmp(argv[i], "--png") || !strcmp(argv[i], "--svg") ||
|
||||
!strcmp(argv[i], "--xpm") || !strcmp(argv[i], "--root")) {
|
||||
@@ -217,6 +278,8 @@ int main(int argc, char *argv[])
|
||||
return PMUSR_WRONG_STARTUP_SYNTAX;
|
||||
}
|
||||
|
||||
const bool batchMode = graphicsOutput || asciiOutput;
|
||||
|
||||
// read startup file
|
||||
std::string startup_path_name{};
|
||||
std::unique_ptr<TSAXParser> saxParser = std::make_unique<TSAXParser>();
|
||||
@@ -227,11 +290,9 @@ int main(int argc, char *argv[])
|
||||
} else {
|
||||
startup_path_name = startupHandler->GetStartupFilePath().Data();
|
||||
saxParser->ConnectToHandler("PStartupHandler", startupHandler.get());
|
||||
//status = saxParser->ParseFile(startup_path_name);
|
||||
// parsing the file as above seems to lead to problems in certain environments;
|
||||
// use the parseXmlFile function instead (see PStartupHandler.cpp for the definition)
|
||||
// parsing the file via ParseFile() seems to lead to problems in certain
|
||||
// environments; use parseXmlFile (see PStartupHandler.cpp) instead
|
||||
status = parseXmlFile(saxParser.get(), startup_path_name.c_str());
|
||||
// check for parse errors
|
||||
if (status) { // error
|
||||
std::cerr << std::endl << ">> musrview **WARNING** Reading/parsing musrfit_startup.xml failed.";
|
||||
std::cerr << std::endl << ">> Any graph will appear with random symbols and colors!";
|
||||
@@ -265,13 +326,13 @@ int main(int argc, char *argv[])
|
||||
musrview_error_msg(errMsg.str());
|
||||
return status;
|
||||
}
|
||||
|
||||
// make a plot list vector
|
||||
PMsrPlotList *msrPlotList = msrHandler->GetMsrPlotList();
|
||||
PIntVector plotList;
|
||||
bool runPresent;
|
||||
for (unsigned int i=0; i<msrPlotList->size(); i++) {
|
||||
for (unsigned int j=0; j<msrPlotList->at(i).fRuns.size(); j++) {
|
||||
// check that run is not already in the plotList
|
||||
runPresent = false;
|
||||
for (unsigned int k=0; k<plotList.size(); k++) {
|
||||
if (msrPlotList->at(i).fRuns[j] == static_cast<int>(plotList[k])) {
|
||||
@@ -279,9 +340,8 @@ int main(int argc, char *argv[])
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!runPresent) {
|
||||
if (!runPresent)
|
||||
plotList.push_back(static_cast<int>(msrPlotList->at(i).fRuns[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +366,8 @@ int main(int argc, char *argv[])
|
||||
// generate the necessary histogramms for the view
|
||||
std::unique_ptr<PRunListCollection> runListCollection;
|
||||
if (result == PMUSR_SUCCESS) {
|
||||
// feed all the necessary histogramms for the view
|
||||
runListCollection = std::make_unique<PRunListCollection>(msrHandler.get(), dataHandler.get(), theoAtData);
|
||||
for (unsigned int i=0; i<msrHandler->GetMsrRunList()->size(); i++) {
|
||||
// if run is in plotList add it, otherwise go to the next
|
||||
runPresent = false;
|
||||
for (unsigned int j=0; j<plotList.size(); j++) {
|
||||
if (static_cast<unsigned int>(plotList[j]) == i+1) {
|
||||
@@ -332,110 +390,140 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
|
||||
if (result == PMUSR_SUCCESS) {
|
||||
// create the ROOT application needed for PMusrCanvas
|
||||
// and pass it only essential arguments
|
||||
std::vector<char*> args;
|
||||
args.push_back(argv[0]); // program name
|
||||
if (graphicsOutput || asciiOutput) {
|
||||
args.push_back((char*)"-b"); // batch mode flag
|
||||
}
|
||||
int cc = args.size();
|
||||
TApplication app("App", &cc, args.data());
|
||||
|
||||
std::vector<PMusrCanvas*> canvasVector;
|
||||
PMusrCanvas *musrCanvas = nullptr;
|
||||
|
||||
bool ok = true;
|
||||
for (unsigned int i=0; i<msrHandler->GetMsrPlotList()->size(); i++) {
|
||||
|
||||
if (startupHandler)
|
||||
musrCanvas = new PMusrCanvas(i, msrHandler->GetMsrTitle()->Data(),
|
||||
10+i*100, 10+i*100, 800, 600,
|
||||
startupHandler->GetFourierDefaults(),
|
||||
startupHandler->GetMarkerList(),
|
||||
startupHandler->GetColorList(),
|
||||
graphicsOutput||asciiOutput,
|
||||
fourier, avg, theoAtData);
|
||||
else
|
||||
musrCanvas = new PMusrCanvas(i, msrHandler->GetMsrTitle()->Data(),
|
||||
10+i*100, 10+i*100, 800, 600,
|
||||
graphicsOutput||asciiOutput,
|
||||
fourier, avg, theoAtData);
|
||||
|
||||
if (musrCanvas == nullptr) {
|
||||
errMsg << ">> musrview **SEVERE ERROR** Couldn't invoke all necessary objects, will quit.\n";
|
||||
std::cerr << errMsg.str();
|
||||
if (show_errMsgBox)
|
||||
musrview_error_msg(errMsg.str());
|
||||
ok = false;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!musrCanvas->IsValid()) {
|
||||
errMsg << ">> musrview **SEVERE ERROR** Couldn't invoke all necessary objects, will quit.\n";
|
||||
std::cerr << errMsg.str();
|
||||
if (show_errMsgBox)
|
||||
musrview_error_msg(errMsg.str());
|
||||
ok = false;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
delete musrCanvas;
|
||||
break;
|
||||
}
|
||||
|
||||
musrCanvas->SetTimeout(timeout);
|
||||
|
||||
// ugly but rootcling cannot handle the spirit-parser framework
|
||||
musrCanvas->SetMsrHandler(msrHandler.get());
|
||||
musrCanvas->SetRunListCollection(runListCollection.get());
|
||||
|
||||
musrCanvas->UpdateParamTheoryPad();
|
||||
musrCanvas->UpdateDataTheoryPad();
|
||||
musrCanvas->UpdateInfoPad();
|
||||
|
||||
if (!musrCanvas->IsValid()) { // something went wrong
|
||||
ok = false;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
musrCanvas->Connect("Done(Int_t)", "TApplication", &app, "Terminate(Int_t)");
|
||||
|
||||
if (graphicsOutput) {
|
||||
musrCanvas->SaveGraphicsAndQuit(fileName.c_str(), graphicsExtension.c_str());
|
||||
}
|
||||
|
||||
if (asciiOutput) {
|
||||
// generate export data file name
|
||||
TString str(fileName.c_str());
|
||||
str.Remove(str.Last('.'));
|
||||
str += ".dat";
|
||||
// save data in batch mode
|
||||
musrCanvas->ExportData(str.Data());
|
||||
musrCanvas->Done(0);
|
||||
}
|
||||
|
||||
// keep musrCanvas objects
|
||||
canvasVector.push_back(musrCanvas);
|
||||
}
|
||||
|
||||
// check that everything is ok
|
||||
if (ok)
|
||||
app.Run(true); // true needed that Run will return after quit so that cleanup works
|
||||
|
||||
// clean up
|
||||
char canvasName[32];
|
||||
for (unsigned int i=0; i<canvasVector.size(); i++) {
|
||||
// check if canvas is still there before calling the destructor **TO BE DONE**
|
||||
snprintf(canvasName, sizeof(canvasName), "fMainCanvas%d", i);
|
||||
if (gROOT->GetListOfCanvases()->FindObject(canvasName) != nullptr) {
|
||||
canvasVector[i]->~PMusrCanvas();
|
||||
}
|
||||
}
|
||||
canvasVector.clear();
|
||||
if (result != PMUSR_SUCCESS) {
|
||||
plotList.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
// clean up
|
||||
// A ROOT application is needed so that the web display and the graphics
|
||||
// export find a working environment.
|
||||
//
|
||||
// In the interactive case no TCanvas is created at all - the spectra travel
|
||||
// to JSROOT as a list of drawables - hence ROOT never opens a window of its
|
||||
// own. Only the graphics export builds a TCanvas, and that runs in batch.
|
||||
std::vector<char*> args;
|
||||
args.push_back(argv[0]);
|
||||
if (batchMode)
|
||||
args.push_back(const_cast<char*>("-b"));
|
||||
int cc = static_cast<int>(args.size());
|
||||
TApplication app("musrview", &cc, args.data());
|
||||
|
||||
std::vector<std::unique_ptr<musrview::PMusrWebCanvas>> canvasList;
|
||||
|
||||
bool ok = true;
|
||||
for (unsigned int i=0; i<msrHandler->GetMsrPlotList()->size(); i++) {
|
||||
auto canvas = std::make_unique<musrview::PMusrWebCanvas>(
|
||||
static_cast<Int_t>(i), msrHandler->GetMsrTitle()->Data(),
|
||||
1200, 900,
|
||||
startupHandler ? startupHandler->GetFourierDefaults() : PMsrFourierStructure{},
|
||||
startupHandler ? startupHandler->GetMarkerList() : PIntVector{},
|
||||
startupHandler ? startupHandler->GetColorList() : PIntVector{},
|
||||
batchMode, fourier, avg, theoAtData);
|
||||
|
||||
// ugly but rootcling cannot handle the spirit-parser framework
|
||||
canvas->SetMsrHandler(msrHandler.get());
|
||||
canvas->SetRunListCollection(runListCollection.get());
|
||||
|
||||
if (!canvas->Build()) {
|
||||
errMsg << ">> musrview **ERROR** " << canvas->GetLastError() << "\n";
|
||||
std::cerr << errMsg.str();
|
||||
if (show_errMsgBox)
|
||||
musrview_error_msg(errMsg.str());
|
||||
ok = false;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
canvas->SetTimeout(timeout);
|
||||
canvasList.push_back(std::move(canvas));
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
canvasList.clear();
|
||||
plotList.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
if (graphicsOutput) {
|
||||
for (auto &canvas : canvasList) {
|
||||
if (!canvas->SaveGraphics(fileName, graphicsExtension)) {
|
||||
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (asciiOutput) {
|
||||
// generate the export data file name
|
||||
TString str(fileName.c_str());
|
||||
str.Remove(str.Last('.'));
|
||||
for (size_t i=0; i<canvasList.size(); i++) {
|
||||
TString name(str);
|
||||
if (canvasList.size() > 1) {
|
||||
name += "_";
|
||||
name += static_cast<Int_t>(i);
|
||||
}
|
||||
name += ".dat";
|
||||
if (!canvasList[i]->ExportData(name.Data())) {
|
||||
std::cerr << std::endl << ">> musrview **ERROR** " << canvasList[i]->GetLastError() << std::endl;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!batchMode) {
|
||||
const std::string where = musrview_select_display(display);
|
||||
|
||||
// one window per PLOT block, exactly as before
|
||||
ROOT::RWebWindowsManager::SetSingleConnMode(false);
|
||||
|
||||
for (auto &canvas : canvasList) {
|
||||
if (!canvas->Show(where)) {
|
||||
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
// run until every window has been closed, or until the timeout expired
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
gSystem->ProcessEvents();
|
||||
gSystem->Sleep(20);
|
||||
|
||||
bool allDone = true;
|
||||
for (auto &canvas : canvasList) {
|
||||
if (!canvas->IsDone()) {
|
||||
allDone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDone)
|
||||
break;
|
||||
|
||||
if (timeout > 0) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::seconds>(now-start).count() >= timeout) {
|
||||
std::cout << std::endl << ">> musrview: timeout of " << timeout << " s reached, terminating." << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End the process here. Tearing down an RWebWindow which drives the
|
||||
// embedded display blocks, because the Qt event loop ROOT started on our
|
||||
// behalf is never stopped again. Everything of interest has been written by
|
||||
// now, so leave immediately rather than hang after the last window closed.
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
gSystem->Exit(result, kFALSE);
|
||||
}
|
||||
|
||||
// only reached in batch mode, where no window was ever opened
|
||||
canvasList.clear();
|
||||
plotList.clear();
|
||||
|
||||
return result;
|
||||
|
||||
Reference in new issue
Block a user