Adding new interface
Sometimes you need to add a whole new concept to VisIt. For concreteness, I'll describe what it takes to add a single button, the button to create a "Named Selection", which identifies a subset of elements that are noteworthy. There's a lot of infrastructure. You'll need to:
- Provide an interface to set the controls
- Both GUI and CLI
- Set up the Viewer so that it can receive this new control information
- Create an RPC so the viewer can communicate it to the engine
- Have the engine act on it.
This page gives an overview of the changes to make. If you don't know or care about named selections, don't worry ... that is just meant to be an example.
Contents
Set up the viewer to receive information
Creating an RPC
The GUI and CLI link in a library called the ViewerProxy. This ViewerProxy communicates with the viewer itself through remote procedure calls (RPCs). The first step is to add a new RPC for your concept. (Example: creating a named selection) You do this running "xmledit" on "ViewerRPC.xml" in /src/viewer/rpc. You modify the enumerated type "ViewerRPCType" to have an entry for your concept. (Example: CreateNamedSelection) NOTE: do not put your RPC last in the list. The last RPC should always be "MaxRPC".
After you're done, run
xml2atts -clobber ViewerRPC.xml
What does this do? It allows your user interfaces (GUI, CLI) to specify this RPC type when it wants to do something related to your concept. (Example: relay the command to the engine that we need to create a named selection.)
Extending ViewerMethods
VisIt clients use the ViewerProxy object to access state coming from VisIt as well as to access methods that cause VisIt to perform various operations. You've extended ViewerRPC to include a new RPC that you want to handle in the viewer. First, you must make the RPC callable via the ViewerMethods class.
The ViewerMethods class is a class that lets VisIt clients easily call various methods, while packing the arguments to those methods into a ViewerRPC object that is sent to VisIt's viewer for processing. Here is an example:
void
ViewerMethods::CreateNamedSelection(const std::string &selName)
{
state->GetViewerRPC()->SetRPCType(ViewerRPC::CreateNamedSelectionRPC);
state->GetViewerRPC()->SetStringArg1(selName);
state->GetViewerRPC()->Notify();
}
The ViewerRPC object contains various general purpose fields that you can use to contain your new method's arguments. You can encode them in any manner you wish so long as you set the RPC type to your new RPC. Once you finish setting the RPC type and putting arguments into the ViewerRPC object, call its Notify() method to send the RPC to the viewer for processing. When you write your viewer-side RPC implementation, be sure to extract the methods in the same fashion as you encoded them.
Also, there is a pure Java implementation of the ViewerProxy. Don't forget to make the same changes to it's ViewerRPC and ViewerMethods objects.
cd src/java ../bin/xml2java -clobber ../viewer/rpc/ViewerRPC.xml
Note that you also need to update the Python code ... but that is described in a separate selection below.
Handling your RPC
In the file /src/viewer/main/ViewerSubject.C, there is a giant switch statement that handles each and every RPC. This switch statement is in the method "HandleViewerRPC". Most of these methods simply call a method to handle the RPC. Note that the viewer is made up of many modules and the handling of the RPC typically consists of telling the appropriate module that an RPC was called. In addition, the arguments to the RPC are packed into some vanilla data members. It is the job of the ViewerSubjectRPC to assign semantics to these vanilla data members as it relays the RPC to the helper modules.
Example of handling an RPC
Imagine a CreateNamedSelectionRPC was received.
You would need to extend the method ViewerSubject::HandleViewerRPC() to support this RPC. It would simply hand the information off to the most appropriate module, in this case the ViewerEngineManager.
case ViewerRPC::CreateNamedSelectionRPC: CreateNamedSelection(); break;
Some of the helper methods don't do much, but this one would have to do error checking, etc:
void ViewerSubject::CreateNamedSelection() { std::string selName = GetViewerState()->GetViewerRPC()->GetStringArg1(); // // Perform the RPC. // ViewerWindow *win = ViewerWindowManager::Instance()->GetActiveWindow(); ViewerPlotList *plist = win->GetPlotList(); intVector plotIDs; plist->GetActivePlotIDs(plotIDs); if (plotIDs.size() <= 0) { Error(tr("To create a named selection, you must have an active" " plot. No named selection was created.")); return; } if (plotIDs.size() > 1) { Error(tr("You can only have one active plot when creating a named" " selection. No named selection was created.")); return; } ViewerPlot *plot = plist->GetPlot(plotIDs[0]); int networkId = plot->GetNetworkID(); const EngineKey &engineKey = plot->GetEngineKey(); TRY { if (ViewerEngineManager::Instance()->CreateNamedSelection(engineKey, networkId, selName)) { Message(tr("Created named selection")); } else { Error(tr("Unable to create named selection")); } } CATCH2(VisItException, e) { char message[1024]; SNPRINTF(message, 1024, "(%s): %s\n", e.GetExceptionType().c_str(), e.Message().c_str()); Error(message); } ENDTRY }
Extending the engine
This new concept (example: creating named selections) needs to percolate all the way to the engine. That means that:
- There must be an interface for the concept
- The engine must be able to support the concept.
The interface portions means that:
- The module "ViewerEngineManager" must be extended to support the concept.
- The ViewerEngineManager can make an appropriate call of the EngineProxy
- The EngineProxy can communicate with the engine through an appropriate RPC.
The second bullet above (the engine must be able to support the concept) is addressed by having an RPC handler on the engine.
Extending ViewerEngineManager
You must create a new method that the ViewerSubject can call.
bool ViewerEngineManager::CreateNamedSelection(const EngineKey &ek, int id, const std::string &selName) { ENGINE_PROXY_RPC_BEGIN("ApplyNamedSelection"); engine->CreateNamedSelection(id, selName); ENGINE_PROXY_RPC_END_NORESTART_RETHROW2; }
Adding a new RPC
Your new concept will need a new RPC (example: an RPC to tell the engine it needs to create a named selection). This RPC is added to src/engine/rpc. The majority of the work here is to define the right data members. In the case of "creating a named selection", that means passing along the selection name, and the plot id to create the selection from.
This class can be a bit tricky to implement. The class itself is something that can be passed from the viewer to the engine. So you have to play by the "right rules". Specifically, make sure that the constructor provides a string that correctly describes the attributes ("i*si" = vector<int>, string, int) and that each of the Set methods make "Select" calls and that SelectAll is correctly implemented. There are plenty of examples, so make sure to look at those.
Extending EngineProxy
The engine proxy is located in src/engine/proxy. It provides an interface for how to control the engine that is linked into the viewer. You need to add a method to the proxy that the viewer can call (example: CreateNamedSelection). The implementation of that method will invoke your new RPC.
ALSO, VERY IMPORTANT: you will need to instantiate your new RPC as a data member of the engine proxy class. Further, you will need to add it to the xfer (pronounced "transfer") object. The order it gets added is important, so make sure it is consistent between the EngineProxy and the Engine itself.
void EngineProxy::CreateNamedSelection(int id, const std::string selName) { std::vector<int> idlist; idlist.push_back(id); namedSelectionRPC(NamedSelectionRPC::CREATE, idlist, selName); if (namedSelectionRPC.GetStatus() == VisItRPC::error) { RECONSTITUTE_EXCEPTION(namedSelectionRPC.GetExceptionType(), namedSelectionRPC.Message()); } }
RPCs on the actual engine
The "Engine" module located in src/engine/main contains the matching RPC modules that talk with the ones in the EngineProxy. Extend this class to have your RPC. Remember that order the RPC objects are instantiated and added to xfer must match what happens in the EngineProxy. Also, remember to register a function with the RPCExecutor. This is what will be called when your RPC is received.
void Engine::SetUpViewerInterface(...) { ... namedSelectionRPC = new NamedSelectionRPC; ... xfer->Add(namedSelectionRPC); ... rpcExecutors.push_back(new RPCExecutor<NamedSelectionRPC>(namedSelectionRPC)); ... }
The last step is to write an RPCExecutor. It goes in src/engine/main/Executors.h. This function receives the RPC, gets out the parameters and relays them to the appropriate module on the engine, likely the NetworkManager.
template<> void RPCExecutor<NamedSelectionRPC>::Execute(NamedSelectionRPC *rpc) { Engine *engine = Engine::Instance(); NetworkManager *netmgr = engine->GetNetMgr(); debug2 << "Executing NamedSelectionRPC." << endl; TRY { netmgr->CreateNamedSelection(rpc->GetPlotIDs()[0], rpc->GetSelectionName()); rpc->SendReply(); } CATCH2(VisItException, e) { rpc->SendError(e.Message(), e.GetExceptionType()); } }
Providing an interface (GUI)
Providing an interface (CLI)
Regenerating PyViewerRPC
When you modify the ViewerRPC file, you must also regenerate an automatically generated file for the CLI.
cd src/cli/visitpy ../../bin/xml2python -clobber ../../viewer/rpc/ViewerRPC.xml
Setting up the ViewerRPC Arguments
Our interface to ViewerRPCs has many "vanilla" data members ("IntArg1", "StringArg1", "IntArg2", etc.). For a given RPC, only some of these data members are used. You need to tell the CLI which arguments should be used. This is done by modifying /src/cli/common/ViewerRPCArguments.C. In this file there is a method named args_ViewerRPC, which has a giant switch statement, with one case for each RPC. You need to add cases for your new RPCs. These cases should call methods that communicate which arguments are used. (There are plenty of examples!)
PyObject * args_ViewerRPC(ViewerRPC *rpc) { ... switch (rpc->GetRPCType()) { ... case ViewerRPC::CreateNamedSelectionRPC: args = args_CreateNamedSelectionRPC(rpc); break; ... } ... }
static PyObject *args_CreateNamedSelectionRPC(ViewerRPC *rpc) { return ViewerRPC_one_string(rpc->GetStringArg1()); }
Adding Logging
When you perform actions in the GUI, VisIt can create an equivalent script. How does this work? Whenever an RPC is sent to the viewer, it tells the CLI about it. The CLI then decides what the equivalent CLI command is. It does this in /src/cli/common/Logging.C. There is a giant switch statement in the method LogRPCs. You need to extent that method to have cases for your new RPCs. Then create methods that generate the equivalent Python script. That's it!
LogRPC(..) { ... switch (rpc->GetRPCType()) { ... case ViewerRPC::CreateNamedSelectionRPC: log_CreateNamedSelectionRPC(rpc, str); break; ... } ... }
static void log_CreateNamedSelectionRPC(ViewerRPC *rpc, char *str) { SNPRINTF(str, SLEN, "CreateNamedSelection(\"%s\")\n", rpc->GetStringArg1().c_str()); }
Adding Documentation
For any method in the CLI, you can obtain online help by running "help(MethodName)" from the CLI interpreter. The text for this online help comes from /src/cli/common/MethodDoc.C.
Modify MethodDoc.h:
- Add the definition of a string for your RPC.
extern const char *visit_CreateNamedSelection_doc;
Modify MethodDoc.C:
- Define the string.
const char *visit_CreateNamedSelection_doc = "CreateNamedSelection \n" "-Creates a named selection from the active plot.\n" "\n" "\n" "Synopsis:\n" "\n" "CreateNamedSelection(name) -> integer\n" "\n" "\n" "Arguments:\n" "\n" "name\n" "The name of a named selection.\n" "\n" "\n" "Returns:\n" "\n" "The CreateNamedSelection function returns 1 for success and 0 for failure.\n" "\n" "\n" "Description:\n" "\n" "Named Selections allow you to select a group of elements (or particles).\n" "One typically creates a named selection from a group of elements and then\n" "later applies the named selection to another plot (thus reducing the\n" "set of elements displayed to the ones from when the named selection was\n" "created).\n" "\n" "\n" "Example:\n" "\n" "% visit -cli\n" "db = \"/usr/gapps/visit/data/wave*.silo database\"\n" "OpenDatabase(db)\n" "AddPlot(\"Pseudocolor\", \"pressure\")\n" "AddOperator(\"Clip\")\n" "c = ClipAttributes()\n" "c.plane1Origin = (0,0.6,0)\n" "c.plane1Normal = (0,-1,0)\n" "SetOperatorOption(c)\n" "DrawPlots()\n" "\n" "CreateNamedSelection(\"els_above_at_time_0\")\n" "TimeSliderSetState(40)\n" "RemoveLastOperator()\n" "ApplyNamedSelection(\"els_above_at_time_0\")\n" ;
Hooking up your functions
You now need to register functions to perform your new actions. You will add them to /src/cli/common/visitmodule.C to the function "AddDefaultMethods()".
You need to add a call to "AddMethod()" for your actions.
AddMethod("CreateNamedSelection", visit_CreateNamedSelection, visit_CreateNamedSelection_doc);
And the definition should parse out the arguments and use them when calling ViewerMethods:
STATIC PyObject * visit_CreateNamedSelection(PyObject *self, PyObject *args) { ENSURE_VIEWER_EXISTS(); char *selName; if (!PyArg_ParseTuple(args, "s", &selName)) return NULL; // Activate the database. MUTEX_LOCK(); GetViewerMethods()->CreateNamedSelection(selName); MUTEX_UNLOCK(); // Return the success value. return IntReturnValue(Synchronize()); }