CARTA Architecture
Overview
CARTA architecture can roughly be divided into two pieces, the Controller/Model/Persistence layer (written in C++ ) and the client view layers. Currently, three client views have been written: a desktop client, a web client, and a Python scripting client. Both the desktop client and web client share the same JavaScript code base. The Controller part of the architecture has the responsibility of accepting requests from any of the client views, contacting appropriate plugins for information needed to process the requests (usually by translating the request into an appropriate model), and then persisting the results of the request to state so that the clients will receive updates. Most of the functionality in CARTA has been delegated to plugins so that it can easily be replaced if faster, customized, or new ways of producing the results are found. Plugin architecture will be discussed more thoroughly in the Plugin Architecture section. Both the desktop and web client share a common connector interface. The web client implements this connector interface using proprietary software called PureWeb, which is basically a wrapper around a Java servlet. The connection architecture will also be discussed more thoroughly later in the subsection Connecting the GUI Clients. An overview of the architecture is provided in the diagram, below.
State Persistance
One of the high level requirements for CARTA is that it maintain and persist state. CARTA has three major clients: a desktop client, a web client, and a Python client. Maintaining state means that a user can write Python scipting commands and see the results of those commands on the desktop GUI. Conversely, it also means that the results of user-interaction with the desktop GUI can also be obtained through the Python client. However, CARTA not only maintains memory state across different clients, it also persists state between user session. Thus, a user should be able to set-up the UI according to his or her preferences, save the state, and then have those preferences restored when starting another CARTA session. In this section, we discuss some of the architectural features that make persistant state possible.
The two files at the heart of state management and persistance are ObjectManager and StateInterface in the State folder. A simplified UML diagram of the major classes in these files is given, below.
The singleton ObjectManager is responsible for creating, managing, and destroying all CartaObjects. Some of its main methods are responsible for managing object state such as "createObject" and "destroyObject". Because it is a container for all CartaObjects, it is able to return a string representation of the state of the application through its "getStateString" method that can be written to a file. When a user wants to restore state from a previous session, the ObjectManager's "restoreSnapshot" method parses the string representation of application state and creates/notifies appropriate CartaObjects of their new state.
All objects responsible for managing state in CARTA inherit from CartaObject. Examples of such classes are Histogram, Profiler, and Stack, but there are many other such classes in the application. Each CartaObject can return its state as a JSON string through the method "getStateString". It also has "resetState" methods for restoring state of various types. The SnapshotType can either refer to "Layout", "Preferences", or "Data". As a rough guideline, the "Layout" snapshot preserves the GUI layout (location and types of windows), a "Preferences" snapshot preserves user preferences such as the jump behavior of an animator, and the "Data" snapshot preserves everything including images that have been loaded. While one of the main functions of a CartaObject is persistence, another very important one is communication with the desktop and client views. The methods "addCommandCallback", "addStateCallback", and "registerView" are concerned with establishing communication channels with the GUI clients. Please see Connecting the GUI Clients for more details on these methods.
Finally all CartaObjects have one or more StateInterfaces. Simpler CartaObjects that only have a preference state will only own one StateInterface, generally called m_state. CartaObjects that need to manage data such as what images are loaded will also tend to have a second StateInterface, generally called m_stateData. StateInterface objects have a very useful "toString" method that returns their state as a JSON string. It is this JSON string that is sent by connectors to the GUI clients. Because it is not efficient to send large string objects to the clients when only one (key,value) pair in the state has changed, CartaObjects that have large states or that have states that tend to change rapidly compared to others (such as mouse event states) may have additional StateInterface objects that they manage. StateInterface objects can also restore their states through their "setState" method. They also have templated convenience methods for setting ("setValue") and getting ("getValue") individual (key,value) pairs that they store. Values can be stored as doubles, integers, QStrings, and JSON objects. The method "flushState" writes any state changes to the connectors so that the GUI clients will receive state updates.
Control Layer
The control layer is not a single class, but a collection of classes that handle requests from the various clients, process the requests by either calling plugins or algorithms in the model layer, and write the results of the requests to state so that the results are visible to clients. An overview of the major classes that compose the control layer is given in the diagram, below.
In addition to being CartaObjects, most of the classes in the control layer also implement the interface, "ILinkable". This is shown in the diagram by the comment, "[ILinkable"], under each class name rather than by using an arrow from each class to the "ILinkable" interface at the bottom of the diagram to make the diagram easier to read. The ILinkable interface is an important one to the design of CARTA. While Carta has a few predesigned layouts, there was a desire to make the layout as customizable as possible. For example, an astronomer might want to view two images, each with a separate animator. The concept of links was introduced in order for astronomers to specify connections between windows (and subsequently, links between C++ objects). Classes that implement ILinkable can have links to other CartaObjects. The method "addLink" is called to add a link to another class while "removeLink" will remove the link. Each ILinkable class can choose what type of classes it can be linked to. In practice, the Controller is the most common parameter to addLink since it is the central class for viewing images and most of the other classes need information about images.
All of the major classes in the control layer have three important methods that are useful to look at when determining what functionality they support: "_initialize", "_initializeCallbacks", and usually a plethora of getters and setters for the (key,value) pairs that constitute their state. In the diagram, above, the methods are indicated only in the Controller class for simplicity; but in fact, all the control layer classes have these methods. State is initialized in the "_initialize" method, so one can look there to determine what state the class manages. The "_initializeCallbacks" is useful to look at if one is working on communication between the control layer and one of the JavaScript views because the callbacks listed in the method give the commands (with parameters) that can be issued from the JavaScript side to set state. Finally, the Python Client will directly call the getters and setters in the control layer to update state.
The Controller itself is the major class in charge of displaying images. Much of its functionality has been delegated to member classes GridControls, ContourControls, RegionControls, and Stack . The remaining classes listed in the diagram above have a one-to-one correspondence to major windows in the GUI: Animator, Colormap, Statistics, Profiler, Histogram, ImageContext, and ImageZoom. The control layer classes communicate with each other via their links, and they are instance variables of the ViewManager which is responsible for their creation and destruction.
We now examine the Stack functionality withen the Controller, which is responsible for displaying a stack of images in a single window. An overview of the Stack architecture is provided in the diagram below. At the heart of an image stack are the classes Layer, LayerData, and LayerGroup, which follow a composite pattern. That is, a LayerGroup is a Layer that can have one or more LayerGroups or LayerDatas as children. The LayerData class is the leaf class in the composite pattern and represents a single image. A Stack itself is a LayerGroup where the children are indexed rather than being an unordered collection. Through this architecture, we are able to address the Carta requirement that images can be grouped into a series of RGB layers. The indexed Stack structure also allows animation through the indexed layers of images in a stack.
Each of the Layers has a specialized class for drawing. For example, the DrawSynchronizer manages rendering for a single LayerData, combining the image produced by a DataSource with vector graphics such as a grid and contour lines. The DrawGroupSynchronizer sets the transparency of each Layer in the group, combining them into a single composite image. Finally, the DrawStackSynchronizer combines its images with vector graphics in an indexed order to produce the final image that the user will see after loading a stack of images into a single window.
The DataSource class basically wraps an image cube. It is responsible for contacting a plugin to load the image, provides methods for setting display properties of an image such as the coordinate system, the axes and frames withen those axes to view, and returns information about its image such as dimensions. It is also responsible for rendering the image. Because it deals directly with the image, there is opportunity in this class to implement efficent algorithms and caching to improve performance.
Plugin Architecture
One of the design goals of CARTA was to make it as extensible as possible. Much of the CARTA functionality is implemented in plugins with the idea that the functionality could be replaced, removed, or reimplemented for a customized application. In particular, CARTA relies on casacore in order to load images and compute histograms, profiles, and statistics. All of this functionality has been delegated to plugins so that in theory, CARTA could be run without using any of the code contained in casacore. Other functionality that has been delegated to plugins includes an extended colormap system, one-dimensional fitting, and region parsing in both CASA and Ds9 formats. There have already been several developer documents written on how to write a plugin in CARTA including: How Plugins Work in CARTA, Plugin Structure, plugin.json, and Writing an Image Plugin for CARTA in C++. Thus, in this section we will just give a brief overview of the plugin architecture through an example class diagram of the classes involved in computing a histogram.
One of the requirements for writing a plugin, is to subclass BaseHook in order to define the data that must be passed to the plugin and the type of result that will be returned by the plugin. The directory carta/cpp/CartaLib/Hooks contains a list of hooks available in CARTA. If there is an exisiting hook that has the proper input and output parameters, it can be used with a new plugin. Otherwise, a new hook class should be written for the plugin. When writing a new hook class, it is important to include the id for the hook in the static list of HookIds. A class diagram of the Histogram hook and other example hooks is given, below.
In order to invoke a plugin to perform a job, an instance of the singleton PluginManager must be obtained. The PluginManager performs a central role in CARTA plugin architecture. On startup, it searches preset paths to obtain a list of plugins that have been installed; for each plugin, it stores the hooks (by id) that each plugin will respond to by invoking the plugin's "getInitialHookList" method. As an example, the Histogram CartaObject must obtain a list of (bin,count) pairs that represent the histogram of an image. Because histogram is computationally expensive, the task must be performed asynchronously. The Histogram object makes a HistogramRenderRequest to the HistogramRenderService. When the HistogramRenderService is not busy fielding other requests, it pulls the HistogramRenderRequest off a queue and invokes a separate process, HistogramRenderWorker, to compute the histogram. The HistogramRenderWorker then passes the necessary data (region, image, frequency range, etc) to the PluginManager through a HistogramHook. The PluginManager then invokes the "handleHook" method of all plugins that have responded with ids identifying the HistogramHook. All plugins must inherit from the IPlugin interface so that they have the mandatory "handleHook" method that performs the plugin work. In the case of the histogram plugin, Histogram1 implements the IPlugin interface. When its "handleHook" method is called, it delegates the work to the ImageHistogram class implementing the IImageHistogram interface. Data is returned from the histogram hook as a HistogramResult, which is defined in the Histogram hook class.
While the above diagram is just an example, all of the plugins are used in basically the same way. The common thread is the passage of a hook class to the PluginManager, which then invokes the "handleHook" method on all plugins registered to handle a particular hook.
Connecting the GUI Clients
Both the C++ side of the code and the JavaScript side of the code have connectors for communication. An overview of the connection architecture is given below. The classes that compose the Javascript side of the connection (both desktop and web) have a suffix of ".js" after their names.
Both the Javascript side of the code and the C++ side of the code have IConnector interfaces, which are implemented by desktop and server classes. One of the main advantages of this architecture is that it allows for writing a single client view in JavaScript that can be used by both the desktop and server clients. There was also the thought that at some point, the proprietary PureWeb code might be replaced, and this design allows for a rewrite of its functionality that would only affect the server connectors on the JavaScript/C++ sides of the code.
When CARTA starts up, either a DesktopPlatform or a ServerPlatform is created based on whether the user is invoking the desktop or server client, respectively. Both the DesktopPlatform and the ServerPlatform implement the IPlatform interface, so for the most part the C++ code does not need to know if it is running the desktop version or the web version of the code. The platform implementation is an instance variable of the singleton Globals class. Thus, a CartaObject wanting to send data to a client view, need only obtain the Globals instance variable and ask it for its IConnector. Similarly on the JavaScript side, an object such as a Histogram or Profile that wants to send a command to the server, need only obtain the IConnector through a global import and then use it to send data to the server. In practice these classes rely on CartaObject to do the actual work of sending the data.
Mention should be made of some of the more important methods in the IConnector interface. There are two very important view related methods, "registerView" and "makeRemoveVGView" that are concerned with supplying images to client views. These two methods will be discussed in more detail in the next section. PureWeb allows for two major methods of communication between client and server. One of these is through "commands" that consist of a text string such as "setClipRange" and a list of parameters such as "clipMin:50,clipMax:100". The only real restriction on the parameter format is that it be a string, but by convention, CARTA is using a uniform format of a comma (,) separator between pairs and a colon separator (:) between keys and values. In order for a CartaObject to receive commands, it must call the "addCommandCallback" method of the IConnector. The CommandCallback that is passed to the method is a lambda expression that should be executed when the command is initiated from the JavaScript side. To see what commands a CartaObject is set up to respond to, one should look for the _initializeCallbacks method in each of the major CARTA classes. Generally, the lambda command callbacks parse any parameters passed in with the command, perform some rudimentary validation and then call an appropriate setter to execute the command. The second major form of communication is through listening to the state of some objects. The "addStateCallback" method of the IConnector interface can be called to register a lambda expression as a StateChangedCallback to be called if the state of a particular variable changes. CartaObjects ultimately call the setState method of the IConnector interface to change the value of one of the (key,value) pairs that they manage.
In an ideal situation the IConnector interface and state management, in general, would be independent of implementation. However, PureWeb has influenced some of the design decisions in the IConnector interface through its use of commands and state for communication. Because PureWeb transmits state as JSON objects, the underlying storage mechanism for CARTA objects is also JSON, and one will find JSON used frequently throughout the code.
While there is no hard fast rule as to how a JavaScript object, responsible for representing CARTA state using UI controls, establishes communication with the server. The diagram below is a very common paradigm which illustrates how a small portion of the histogram UI communicates with the server.
Because the UI for many of the widgets is fairly complex, the JavaScript objects responsible for the UI will usually use object composition to delegate responsibility for particular areas of the screen real estate to child objects. This is certainly the case for Histogram.js, which delegates to a Settings.js object, which delegates to a PageRange.js object (among others) which shows up as a tab in the Histogram settings and in turn delegates to a Histogram2D.js object (among others), which is responsible for the two-dimensional foot print of the histogram. The 2D foot print refers to whether the histogram is of the entire image, the current region in the image, or consists of a histogram for each defined region. When the HistogramSettings object is created, it adds a callback to a shared variable that represents of the state of a server-side Histogram objects. The steps for doing this are outlined in the diagram note.
Any time the state of the server-side object changes, the callback will be invoked. Generally, the callback will call setter methods of the child objects. In the diagram, a change in the 2D foot print of a histogram will propagate from the callback through setter methods of child objects until it reaches the "setFootPrint" method of Histogram2D. Histogram2D will use the new value of the foot print to update the UI (radio buttons) with the new server state.
Conversely, if a user selects one of the radio buttons that is a choice for the 2D foot print, this change must be propagated to the server. Methods that send commands to the server in order to update its state generally follow the naming convention, " _send------Cmd", where the "----" represent describe the state that is to be changed on the server. So in this case, "_sendFootPrintCmd", will send a command to the server containing the new foot print that the user has selected from the UI. Inside these methods will be the name of the command and the parameters to be passed with the command. Care must be taken with both the name of the command and parameters since they must match up exactly with how the corresponding callback is defined on the server.
When defining new "setter/_send------Cmd" pairs on the JavaScript side and the corresponding methods that set state on the server-side, care must be taken to avoid creating an infinite loop. Such a scenario can occur if the user sets a value, the new value is sent to the server, the server updates its state with the new value, the callback is invoked on the JavaScript side with the new value, which in turn calls a setter which updates the UI (even though it has not changed) and repeats the whole cycle. Thus, it is important to test values and only set them if they indeed have actually changed.
Image and Plot Views
A special type of server/client communication is concerned with passing images for display from the server to the client. Fundamentally, the images one views in CARTA as well as the plots one sees as a Histogram or Profile are just png or jpeg images displayed in an HTML image tag. On the JavaScript side there is a hierarchy of classes to manage views in the html5/source/class/skel/boundWidgets/View folder. A class diagram of the JavaScript view classes is given, below.
There is a corresponding heirarchy of top level window elements that (optionally) will instantiate and display a view as part of their UI. So, for example, a DisplayWindowImage will instantiate and add a PanZoomView to its UI. DisplayWindowImageZoom and DisplayWindowImageContext also display and manage a PanZoomView of an image. Similarly, DisplayWindowHistogram and DisplayWindowProfile instantiate and a DragView to their UI in order to display plots. Each of these top level windows has the id of the server-side object that manages state for its area of concern (please see Connecting the GUI Clients for a discussion of how this server-side id is obtained). Since view state tends to change very rapidly due to mouse events, the managed view objects do not share the same state as the underlying windows so they must have their own unique server-side ids in order to communicate and obtain updates. By convention, the id of a view for a top level window will match the id of the window with the word "View" suffixed to it. The ViewWithoutInputDivSuffexed class appends this suffix automatically to the id of the server-side object in the containing window without the window having handle the logistics. For example, it might be the case that the server-side Histogram object has a state id of "CartaObject/c17". In which case the view responsible for displaying the histogram plot will communicate with the server, using an id of "CartaObject/c17/View". All server-side object ids begin with the work CartaObject/c with a number that roughly reflects their order of creation. While all Views are displayed by top level windows, not all top level windows display views.
The classes involved in producing histogram and profile plots on the server-side are diagrammed, below. The PlotGenerator sets properties on a Qwt plot and produces a QImage of the plot when requested. This QImage is passed to an ImageView. The ImageView class has methods for processing mouse input, resizing the produced images when a user changes the size of a window , and notifying the connector when the view needs refreshing. The PlotManager handles registering the View with a connector so that the client-side can receive image updates.
Image plots are produced using a different hierarchy, although the same IView interface is used by the DesktopConnector and ServerConnector to send the images to the client.
One of the CARTA requirements is the ability to combine three images into a single RGB layer. The LayerGroup class has a DrawGroupSynchronizer that uses an IQImageCombiner to combine QImages into a single image. Internally, the IQImageCombiner makes use of an AlphaCombiner to set the transparency of a QImage and a PixelMaskCombiner to apply a color mask to a QImage.
The image Stack itself uses the DrawStackSynchronizer to manage stack render requests. A RenderRequest is placed in a queue in the DrawStackSynchronizer to wait until it can be rendered. The DrawStackSynchronizer takes RenderRequests off the queue in first-in first-out order when it finishes a request and is available for the next request. It uses a LayeredViewArbitrary to produce an image that is an ordered combination of the images and vector graphics that make up the stack. The SimpleRemoteVGView implementation of the IView interface then renders the QImage that will be passed by the DesktopConnector and the ServerConnector to the client.
Python Client
On the C++ side, a socket connection is established to listen and send messages to the Python client. Json messages are passed to the MessageListener class, which notifies the ScripedCommandInterpreter when a message arrives. The ScriptedCommandInterpreter parses the messages and then invokes appropriate methods on the ScriptFacade to perform the server-side functions necessary to execute the instructions in the message. The SciptFacade uses the singleton ObjectManager to obtain references to the appropriate CartaObjects responsible for performing the commands. Then the ScriptFacade invokes the necessary methods on the CartaObjects to execute the instructions. Results from the instructions are returned asynchronously to the ScriptedCommandInterpreter, which notifies the MessageListener to send the results as a Json message across the socket to the Python client. A diagram of the major classes involved in the C++ side of the socked communication is given in the diagram below. These classes are contained in the carta/cpp/core/ScriptedClient folder of the project.
The Python classes responsible for maintaining the socket connection to the C++ classes can be found in the folder, carta/scriptedClient/carta. The class, cartavis.py is the Python representation of the CARTA application and the entry point for Python scripts. Through cartavis.py, one can obtain references to various cartaview objects, which represent the high level C++ CartaObjects. For example, calling the method, "getHistogramViews" on the cartavis object will return Python representations of all loaded C++ Histogram objects. Methods withen these high-level Python imageview objects can be invoked to send commands to the corresponding C++ objects and return results. The messages are sent and received through the Python tagconnector object. A class diagram of the major Python client classes is given, below.