Writing an image plugin for CARTA in C++¶
The purpose of this document is to describe how to write a C++ plugin that allows opening images of new file formats in CARTA.
- Setting up build environment
- Writing the plugin
- Testing the plugin
While reading this guide, please refer to appendices B, C and D, which contain some general information about the plugin architecture in CARTA.
Setting up a build environment¶
At the moment we do not have an easy mechanism to set up a build environment for just compiling and testing plugins. That means for now you will need to set up your computer to build all of CARTA. In the future we hope to have a simplified setup just to be able to build plugins. Please follow the instructions in Appendix A on how to setup your machine to be able to compile CARTA and your plugin.
Writing the plugin¶
The easiest way to write a new plugin for CARTA is to take an existing one, clone it and modify it to do what you need. For adding a new image format the best plugin to clone is the qimage plugin, e.g.:
> cd /tmp/xxx/CARTAvis/carta/cpp/plugins
> cp -a qimage myimage
> cd myimage
> mv qimage.pro myimage.pro
… rename the header/sources as you see fit
> emacs plugin.json
… fix up entries to match your plugin
> emacs ../plugins.pro
… add myimage to the list of plugins
> cd ..
> make
At this point you should have a functional plugin which does exactly the same thing qimage plugin does. It is now time to modify the sources to make it do what you want. The best way to go about that is to understand what the existing code does.
The following interface classes are of particular interest, as these are the ones you may need to implement for your plugin to work. They should have sufficient in-code documentation to get you started:
IPlugin
Carta::Lib::NdArray::RawViewInterface
CoordinateFormatterInterface
Carta::Lib::Image::MetaDataInterface
Carta::Lib::Image::ImageInterface
Carta::Core::Hooks::LoadAstroImage
The main class (the one that implements the plugin), looks like this (qimage/QImagePlugin.h
):
/// This plugin can read image formats that Qt supports.
#pragma once
#include "CartaLib/IPlugin.h"
#include <QObject>
#include <QString>
class QImagePlugin : public QObject, public IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.cartaviewer.IPlugin")
Q_INTERFACES( IPlugin)
public:
QImagePlugin(QObject *parent = 0);
virtual bool handleHook(BaseHook & hookData) override;
virtual std::vector<HookId> getInitialHookList() override;
};
Study the implementation of these methods (qimage/QImagePlugin.cpp
).
Appendix A : setting up the build environment¶
Here we try to explain how to setup your machine to be able to build CARTA from sources. You will most likely need to install some prerequisites first, then download the CARTA sources and compile them.
CARTA prerequisites¶
Operating system¶
We highly recommend you do your development on Ubuntu 14.04 LTS 64-bit. The instructions in this document are written for this OS.
Compiler¶
You will need g++ version 4.8.1 or newer. If you have not installed it already, you can:
> sudo apt-get install g++
Casacore¶
You will need casacore 2.0.1 or newer to be able to compile all of CARTA. This can be a lengthy procedure, please read the documentation here:
CARTA’s core does not actually depend on casacore, only some of the bundled plugins do. Therefore it’s possible to adjust the build process so that you don’t need to install casacore at all. This is done by disabling the bundled plugins that require casacore:
- WcsPlotter
- Histogram
- CasaImageLoader
- CasaCore-2.0.01
You can disable them by editing plugins.pro and removing the corresponding entries for these plugins.
Python¶
If you want python plugins to work, you will need to install python dev files:
> sudo apt-get install libpython2.7-dev
You can disable python support by disabling the python273 plugin (you have to edit plugins.pro).
QWT¶
You will need libqwt version 6.1.2 or above. You need to make sure that the qwt you use is compiled against the same version of Qt that you will be using! This is very important. You can get installation instructions here:
Qooxdoo¶
You need to install qooxdoo version 3.5.1 from:
Please don’t install a newer version, as it will likely not work properly. At the time of writing this document, the following link works:
Download CARTA sources:¶
Next you need to download the sources for CARTA from github.
We recommend you create an empty directory and do all your work there.
For example below we create /tmp/xxx
:
> mkdir /tmp/xxx
> cd /tmp/xxx
> github clone git@github.com:Astroua/CARTAvis.git
or use Pavol’s branch:
> github clone git@github.com:pfederl/CARTAvis.git
Next you need to configure CARTA.
- create ../CARTAvis-externals and setup links
- edit common_config.pri
Compile CARTA:
> cd /tmp/xxx
> mkdir build
> cd build
> qmake CARTA_BUILD_TYPE=dev /tmp/xxx/CARTAvis/carta/carta.pro
Run qooxdoo compiler:
> cd /tmp/xxx/CARTAvis/carta/html5/common/skel/
> ./generate.py
Compile C++:
> cd /tmp/xxx/build
> make
Try to run the desktop application:
> ./cpp/desktop/desktop --html /tmp/xxx/CARTAvis/carta/VFS/DesktopDevel/desktop/desktopIndex.html /tmp/some.fits
Appendix B : how plugins work in CARTA¶
The main purpose of a plugin is to extend some aspect of the core viewer functionality. For example, the core could implement some basic colormaps, and a plugin could be used to add additional ones. Or the core could support FITS and CASA input image formats, while a plugin could add support for HDF5. A plugin could also be used to replace/override existing functionality, for example changing the formatting or the type of information present in the status bar. The support for a particular extension will need to be coded into the core. To illustrate how this might work, consider the pseudo code that loads an image from a file. Below is what the implementation might look like without any plugin support:
IImage * loadImage( const string & fileName):
IImage * result = loadFITSorCASA( fileName);
if( result) return result;
reportError( "Could not load image...");
return nullptr; // or throw exception
}
The above code handles only FITS and CASA images. If neither can be used, the core gives up and reports an error to the user. To add support for additional image formats, we could augment the above code like this:
IImage * loadImage( const string & fileName):
IImage * result = loadFITSorCASA( fileName);
if( result) return result;
// core could not handle this format, let's see if one of the plugins can
for( auto plugin : allPlugins_that_implement_loadImage) {
result = plugin-> loadImage( fileName);
if( result) return result;
}
reportError( "Could not load image...");
return nullptr; // or throw exception
}
By adding couple of lines of (pseudo) code, we can now extend supported image formats using plugins. The plugin method ‘loadImage’ is something I like to refer as a hook (vaguely motivated by http://en.wikipedia.org/wiki/Hooking). Hooks are strategically placed pieces of code throughout the core, which call one ore more plugins implementing that particular hook. Some hooks are only called once (for example, there is a hook that is executed when the viewer starts, and another when the viewer exits). Other hooks are executed multiple times (e.g. user is trying to load an image with an unknown format). Some hooks have input parameters, others don’t. Some hooks have results, some don’t. Some hooks are always executed by all plugins that implement them. Some hooks are only executed by the first plugin that returns a valid result.
Since the core of the viewer is written in C++, we decided we’ll support plugins written in C++ (for maximum performance). Each plugin is essentially a C++ code compiled into a shared library. We use Qt’s lower-level plugin mechanism to gain some platform independence (http://qt-project.org/doc/qt-5/plugins-howto.html). Each plugin implements roughly the following interface:
class IPlugin {
public:
virtual std::vector<HookId> getInitialHookList() = 0;
virtual bool handleHook( BaseHook & hookData) = 0;
};
After a plugin is loaded, it’s ‘getInitialHookList()’ method is invoked, which tells the core the list of ‘hooks’ the plugin implements. This is used as an optimization technique, so that we don’t call every single plugin for every hook. The second method ‘handleHook()’ is called when the core is executing a hook. The name of the hook, it’s parameters and the result are all encoded in the single parameter ‘hookData’ of an abstract type BaseHook. Each hook in the core is essentially a subclass of BaseHook. The BaseHook class is very simple:
class BaseHook {
public:
explicit BaseHook( const HookId & id) : m_hookId(id) {}
/// dynamic ID
HookId hookId() const { return m_hookId; }
protected:
HookId m_hookId;
};
The only thing of note here is the HookId type, which is currently an integer. We use these IDs to quickly decide whether we want to do downcast to the proper hook type.
And here is an example of how the loadImage hook might be implemented:
class LoadImageHook : public BaseHook {
public:
typedef IImage * ResultType;
struct Params {
Params( QString p_fileName) {
fileName = p_fileName;
}
QString fileName;
};
enum { StaticHookId = 5 };
LoadImage(Params * pptr) : BaseHook( StaticHookId), paramsPtr( pptr) {}
ResultType result;
Params * paramsPtr;
};
Here is pseudo-code for a C++ plugin that implements only the loadImage hook:
class QImagePlugin : public IPlugin {
virtual std::vector<HookId> getInitialHookList() override;
virtual bool handleHook(BaseHook & hookData) override;
};
std::vector<HookId> QImagePlugin::getInitialHookList() {
return { LoadImageHook::StaticHookId };
}
bool MyPlugin::handleHook(BaseHook & hookData) {
if( hookData.hookId() == LoadImageHook::StaticHookId) {
LoadImageHook & hook = static_cast<LoadImageHook &>( hookData);
hook.result = readSomeFileFormatForExampleHDF5( hook.paramsPtr->fileName);
return hook.result != nullptr;
}
warning << "Sorry, don't know how to handle this hook" << hookData.hookId();
return false;
}
The plugins will be loaded and initialized in the same process/thread as the core viewer. All communication (via handleHook) will also be called from the same thread. If they want, plugins can spawn additional threads or processes, but then it’s up to them to set up synchronization with the core thread. The core won’t provide any such functionality.
At startup all subdirectories of the main ‘plugins’ directory are scanned (by reading and parsing the plugin.json files). All plugins are compiled into a list. The list is then topologically sorted using the specified dependencies. Then each plugin from the sorted list is processed:
- we attempt to load the plugin as native (type=c++ | lib)
first we load all libraries under plugin’s /lib/ directory (matching *.so and *.so.* patterns)
- the loading order of these is determined heuristically, by repeating these steps:
- if the list of libraries to load is empty, break out of this loop
- try the libraries on the list that have not been loaded and load them
- if a library was loaded, mark it as loaded
- if any library was loaded, continue the loop
- if no library was loaded, break out indicating a warning
if the plugin’s type is c++, we load plugin.so, or report error
if the plugin was not loaded as native, we attempt to load it using loadPlugin hook, i.e. using some other plugin
if the plugin was loaded (either as native or foreign), we execute it’s initialize method
Appendix C : plugin structure¶
Each carta plugin lives in its own directory, with the following contents:
plugin.json |
required file | meta-information about the plugin |
libplugin.so |
optional file | the actual C++ plugin |
libs/ |
optional subdirectory | directory of libraries the plugin provides and/or uses |
libs/lib1.so ... |
optional files | the actual libraries the plugin provides/uses. These can be organized in additional subdirectories. Any .so file will be treated as a provided library and will be loaded (if possible). |
other files or subdirectories | optional | anything else the plugin needs? |
Appendix D : plugin.json¶
api: | required integer | which plugin-API does this plugin use |
name: | required string | Unique name by which the plugin will be identified. It is a good idea to name the plugin’s directory the same as this name. |
version: | required string | version as a string in x.y.z format |
type: | required string | type of plugin, currently supported types are: “C++” and “libs”. Other types can be added via plugins (e.g. “python”). |
depends: | optional array of strings | array of other plugins that must be present for this plugin to work |
description: | optional (ascii string) | short description of the plugin’s functionality |
about: | optional (html string) | any other info about the plugin (e.g. home page, authors, organization, etc) |
provides: not implemented yet |
string list | what does the plugin provide, can be used as dependencies |
other: | json object | configuration options for the plugin, not parsed by core. The plugin needs to parse it itself |
Example plugin.json file:
{
"api" : "1",
"name" : "qimage",
"version" : "1",
"type" : "C++",
"description": "Adds ability to load jpeg/png images. (anything Qt supports)",
"about" : "Part of carta. Written by Pavol",
"depends" : [ ]
}
Appendix E: CARTA config file¶
The configuration file is used to change some settings that are installation wide, for example the search paths for plugins. The location of the main configuration file is by default in:
$HOME/.cartavis/config.json
This location can be changed in two ways:
via a command line switch passed to the executable, e.g.
--config /tmp/myconfig.json
note that for the server-side viz this would require modifying the appropriate plugin.xml file in the PureWeb installation
via an environment variable called CARTAVIS_CONFIG_FILE, e.g:
setenv CARTAVIS_CONFIG=/tmp/myconfig.json
If both options are used, the command line takes precedence.
What’s in the config file? For example search paths for plugins:
{
"pluginDirs": [
"$(APPDIR)/../plugins",
"$(HOME)/.cartavis/plugins"
], ...
}
The above config.json tells carta to look for plugins in two places:
- relative to the executable
- relative to the user’s home directory
You can also disable some plugins:
{
"disabledPlugins" : [
"ColormapsPy",
"Colormaps1"
],
}
Or provide particular plugins some settings:
{
"plugins": {
"CyberSKA": {
"vizmanUrl1": "http://localhost:1234/testDir/%%",
"vizmanUrl2": "http://localhost:8080/test.txt"
},
"DevIntegration" : {
"enabled" : true
}
},
}
Appendix F: libraries & versions¶
Here is a list of libraries and their versions that we currently use in development:
qooxdoo | 3.5 |
qwt | 6.1.2 |
qt | 5.3 and 5.4 |
ast lib | 8.0.2 |
casacore | 2.0.1 |
pureweb | 4.1.1 |
wcslib | 4.23 |
cfitsio | 3360 |
rapidjson | from github: https://github.com/drewnoakes/rapidjson/commit/8307f0f4c9035bd63853bdf9e1b951e8c0e37b62 |