Developing OVE Applications¶
OVE provides a number of applications to display commonly-used types of content, such as HTML, tiled images, audio and video files, maps, networks, and charts. The HTML App is particularly flexible, and allows the hosting of general HTML/JavaScript web applications. However, if existing applications do not meet your needs then you can write a new OVE app.
@ove-lib/appbase
provides a base library on which OVE applications can be built.
Application structure¶
An application typically has the structure:
├── README.md
├── package.json
└── src
├── client
│ ├── common
│ │ ├── <app-name>.css
│ │ └── <app-name>.js
│ ├── constants
│ │ └── <app-name>.js
│ ├── control
│ │ ├── <app-name>.css
│ │ └── <app-name>.js
│ ├── index.html
│ └── view
│ ├── <app-name>.css
│ └── <app-name>.js
├── config.json
├── data
├── swagger-extensions.yaml
└── index.js
At the top level, the README.md
file explains the purpose and usage of the application in human-readable form, whereas package.json
provides machine-readable information about the package (name, version, author, license), its dependencies, and build commands.
Within src/
, config.json
describes any pre-configured states
that are provided as examples (which may depend on data files in src/data
); src/index.js
is the file that is actually run by node.js to create a server instance.
The @ove-lib/appbase
base library exposes an API to interact with the app. If the application exposes additional API methods the src/swagger-extensions.yaml
is used to describe them, so that Swagger can automatically generate documentation.
Applications are partitioned into separate a control
and view
, with shared parts placed in src/client/common/
(for CSS or JavaScript functions) or src/client/constants
(for JavaScript constants).
A single index.html
file is shared between the control
and view
, but it renders different content in both cases due to the inclusion of different JavaScript files.
The JavaScript and CSS files in control/
are used to render the application’s control page; the JavaScript and CSS files in view/
are used to render the page that is displayed when the application is loaded into a section.
The index.html file¶
Before the index.html
file is served, the placeholders __OVEHOST__
and __OVETYPE__
are replaced (this replacement is specified by the registerRoutesForContent
function defined in ove-lib-utils/src/index.js
).
__OVEHOST__
is replaced by the host-name that was specified by the OVE_HOST
environment variable set in pm2.json
(or docker-compose.yml
).
__OVETYPE__
is replaced by view
or control
.
This mechanism allows the construction of paths for the inclusion of JavaScript or CSS files.
Logging¶
Messages can be logged by creating a OVE.Utils.Logger
object, and then calling the method with the appropriate level (fatal
, error
, warn
, debug
, info
or trace
):
const { Constants } = require('./client/constants/<app-name>');
const log = OVE.Utils.Logger(Constants.APP_NAME, Constants.LOG_LEVEL);
log.debug('Starting application');
The index.js file and server-side application initialization¶
For most applications the index.js
would look similar to:
const { Constants } = require('./client/constants/<app-name>');
const { app, log } = require('@ove-lib/appbase')(__dirname, Constants.APP_NAME);
const server = require('http').createServer(app);
const port = process.env.PORT || 8080;
server.listen(port);
log.info(Constants.APP_NAME, 'application started, port:', port);
The @ove-lib/appbase
base library also exposes express
, nodeModules
, appState
, clock
and config
. These can be used to register express routes, to expose nodule modules, to access the application specific state, to get the time from a synchronised clock, or to access application-specific configuration found in the config.json
file. To expose a node module from your application:
const { Constants } = require('./client/constants/<app-name>');
const { express, app, log, nodeModules } = require('@ove-lib/appbase')(__dirname, Constants.APP_NAME);
const path = require('path');
log.debug('Using module:', '<module-name>');
app.use('/', express.static(path.join(nodeModules, '<module-name>', '<module-directory>')));
Using the synchronised clock on the server-side¶
The clock synchronisation on OVE uses WebSockets and therefore, it must be initialised appropriately when a WebSocket connection is available. Please also note that the the synchronised time is not available at start-up. It may take up to 5 minutes for each client to perform the first clock synchronisation and it can take a further 10 minutes for the clocks to stabilise. To get the time from a synchronised clock:
const { Constants } = require('./client/constants/<app-name>');
const { clock, log, Utils } = require('@ove-lib/appbase')(__dirname, Constants.APP_NAME);
let socket = new (require('ws'))('ws://' + Utils.getOVEHost());
clock.setWS(Utils.getSafeSocket(socket));
socket.on('open', function () {
clock.init();
});
log.debug('Original time is:', clock.getTime());
setInterval(function () {
log.debug('Synchronised time is:', clock.getTime());
}, 300000);
The swagger-extensions.yaml file¶
The swagger-extensions.yaml
is optional and only found in applications exposing REST API methods. Contents of this file include definitions of Paths and Tags objects according to the Swagger 2.0 Specification.
Server-side Helper methods¶
OVE Utils
provides a number of useful methods, such as Utils.getOVEHost()
, Utils.sendMessage(res, status, message)
, Utils.sendEmptySuccess(res)
, Utils.getSafeSocket(socket)
, and Utils.isNullOrEmpty(value)
. To make use of OVE Utils
from your application to communicate with other OVE components using WebSockets:
const { Constants } = require('./client/constants/<app-name>');
const { log, Utils } = require('@ove-lib/appbase')(__dirname, Constants.APP_NAME);
let ws;
const getSocket = function () {
const socketURL = 'ws://' + Utils.getOVEHost();
log.debug('Establishing WebSocket connection with:', socketURL);
let socket = new (require('ws'))(socketURL);
socket.on('error', log.error);
socket.on('close', function (code) {
log.warn('Lost websocket connection: closed with code:', code);
log.warn('Attempting to reconnect in ' + Constants.SOCKET_REFRESH_DELAY + 'ms');
// If the socket is closed, we try to refresh it.
setTimeout(getSocket, Constants.SOCKET_REFRESH_DELAY);
});
ws = Utils.getSafeSocket(socket);
};
getSocket();
ws.safeSend(JSON.stringify({ appId: Constants.APP_NAME, message: message }));
Clint-side application initialization¶
On document load, you should create a new OVE object attached to the window
object of the web browser:
window.ove = new OVE(Constants.APP_NAME);
// perform initialization
window.ove.context.isInitialized = true;
As part of initialization, you should call OVE.Utils.initControl
or OVE.Utils.initView
:
const initControl = function (data) {
// perform further initialization.
}
OVE.Utils.initControl(Constants.DEFAULT_STATE_NAME, initControl);
OVE.Utils.initControl
accepts a function to perform any further initialization that will be called once OVE initializes the controller of the app. It also accepts the name of the default state
to be loaded as a part
of the initialization process.
const initView = function () {
// perform initialization before OVE loads the viewer.
}
const onStateLoaded = function () {
// called immediately after the state is loaded.
}
OVE.Utils.initView(initView, onStateLoaded);
OVE.Utils.initView
accepts a function to perform initialization before OVE initializes the viewer of the app. Unlike the controller, the viewer needs to be pre-initialized. This is to ensure JavaScript libraries are appropriately initialized before the application state is loaded. OVE.Utils.initView
also accepts a function that gets called immediately after the state is loaded to the viewer.
If there are any further initialization that needs to be done after OVE initializes the viewer of the app, OVE.Utils.initView
also accepts an optional function as its third parameter:
const postInitView = function () {
// perform further initialization.
}
OVE.Utils.initView(initView, onStateLoaded, postInitView);
You should also ensure that page elements have been resized appropriately.
Resizing¶
OVE.Utils
provides two methods for automatically resizing a <div>
element.
OVE.Utils.resizeController(contentDivName)
scales the element with id
contentDivName
to fit inside both the client and window, whilst maintaining the aspect ratio of the section/content; OVE.Utils.resizeViewer(contentDivName)
resizes the element with id
contentDivName
to the size of the corresponding section (which may span multiple clients), and then translated based on the client’s coordinates.
Client-side Helper methods¶
OVE.Utils
provides a number of useful methods, such as OVE.Utils.getQueryParam(name, defaultValue)
, OVE.Utils.getURLQueryParam()
, OVE.Utils.getSpace()
, OVE.Utils.getClient()
, OVE.Utils.getViewId()
and OVE.Utils.getSectionId()
.
OVE.Utils.Coordinates.transform(vector, inputType, outputType)
provides a mechanism to convert coordinates of one format to another. The input and output types can be one of OVE.Utils.Coordinates.SCREEN
, OVE.Utils.Coordinates.SECTION
or OVE.Utils.Coordinates.SPACE
:
// Get location of mouse pointer within the space.
function onMouseEvent(event) {
const spaceCoordinates = OVE.Utils.Coordinates.transform(
[event.screenX, event.screenY], OVE.Utils.Coordinates.SCREEN, OVE.Utils.Coordinates.SPACE);
}
The OVE object¶
The OVE object (window.ove
) provides a number of useful functions and data structures to handle state, to interpret geometry and to communicate via WebSockets. It also provides a context (window.ove.context
) to hold the application’s local variables. The window.ove.context.uuid
property provides a unique identifier for each instance of OVE. This can be used to uniquely identify each viewer and controller in the system. The window.ove.context.hostname
property provides the hostname of the ove.js library.
OVE also ensures clocks of all clients are perfectly synchronised. The window.ove.clock.getTime()
function can be used to obtain the synchronised system time. Clock synchronisation takes time to
complete and the system would take at least a minimum of two minutes for the initial synchronisation
attempt. It may take a further few more attempts before the clocks are properly synchronised.
Handling state¶
The window.ove.state
object provides a window.ove.state.current
data structure to hold the current application state. You can decide what this should contain, given the particular needs of your application.
The current application state (contents of window.ove.state.current
) can be sent as a WebSocket broadcast by calling OVE.Utils.broadcastState()
(with no arguments). This will update the current state on all clients that receive the message; you can register a callback function to be called when this change occurs using OVE.Utils.setOnStateUpdate(callbackFunctionName)
.
The window.ove.state
object also provides two other methods cache
and load
, which can be used to cache the application state on the server and load it sometime later. These methods are internally called by the utility methods provided by OVE.Utils
and therefore their use is limited to a few advanced use-cases.
Interpreting geometry¶
The window.ove.geometry
provides information useful to interpret the geometry of the clients:
window.ove.geometry.x
- Displacement along the x-axis relative to the top-left of the section in pixelswindow.ove.geometry.y
- Displacement along the y-axis relative to the top-left of the section in pixelswindow.ove.geometry.w
- Width of the clientwindow.ove.geometry.h
- Height of the clientwindow.ove.geometry.section.w
- Width of the section (or the total width of the application)window.ove.geometry.section.h
- Height of the section (or the total height of the application)window.ove.geometry.space.w
- Width of the space (or the total width of all clients)window.ove.geometry.space.h
- Height of the space (or the total height of all clients)window.ove.geometry.offset.x
- Displacement of top-left of the client along the x-axis relative to the top-left of the browser window in pixelswindow.ove.geometry.offset.y
- Displacement of top-left of the client along the y-axis relative to the top-left of the browser window in pixels
While all of this information is available on a fully initialized viewer, the controller only has:
window.ove.geometry.section.w
- Width of the section (or the total width of the application)window.ove.geometry.section.h
- Height of the section (or the total height of the application)
Communicating via WebSockets¶
The window.ove.socket
provides two functions send
and on
to send and receive messages using WebSockets:
window.ove.socket.on(function (message) {
// logic to interpret the message
});
window.ove.socket.send(message);
The message
argument represents a JSON serializable object in both methods. These methods are particularly useful to trigger remote operations or to expose JavaScript functionality using REST APIs. They can also be used to develop controllers that support interactive operations such as linking and brushing, across a number of different application instances or types. The tool used for Debugging Communication via WebSockets is a good example on how to use these methods to develop an external controller.
Debugging Communication via WebSockets¶
OVE provides a tool to debug communications via WebSockets. This tool can be obtained either by downloading it (right-click this link and select Save as) or by cloning the source code.
git clone https://github.com/ove/ove
cd ove/packages/ove-core/tools/debug-socket
To access the browser-based tool you will also need to start a web-server. This can be done using one of the approaches shown below.
Node.js:
npm install http-server -g
http-server
Python 2:
python -m SimpleHTTPServer 9999
Python 3:
python3 -m http.server 9999
Please note that you may need to specify a port number if you have chosen to use Python and the default of port 8000 is already in use (the examples above specify 9999 as the port to use).
Once the application is launched it will be available at the any of the URLs printed on the console, if you have chosen to use Node.js or at http://localhost:8000
(or corresponding port number), if you have chosen to use Python.
If the tool prompts you to provide oveHost
, oveAppName
and oveSectionId
as query parameters, please modify the URL and provide these parameters.
The oveHost
parameter takes the form of OVE_CORE_HOST:PORT
. The oveAppName
parameter is the name of the application you are interested in debugging, such as maps
, images
or html
(which by convention is always in lower case). The name of the application can also be obtained via the http://OVE_CORE_HOST:PORT/app/OVE_APP_NAME/name
API. The oveSectionId
is the identifier of the section in which the application is currently deployed in. This identifier is used when accessing the application’s control page or when working with OVE APIs to manage sections.
If the tool has been accessed with the correct parameters, you should see a text box along with a Send
button. The contents of the text box should automatically change when you perform any operation on the application that you are currently debugging. You can modify the contents of the text-box and press the Send
button to control the application from within the tool.
Communicating within a web browser¶
The window.ove.frame
provides two functions send
and on
to send and receive messages within a web browser:
window.ove.frame.on(function (message) {
// logic to interpret the message
});
window.ove.frame.send(target, message, appId);
The message
argument represents a JSON serializable object in both methods. The target
argument can be one of Constants.Frame.PARENT
, Constants.Frame.PEER
or Constants.Frame.CHILD
, and the optional appId
argument identifies the target application. If window.ove.frame.on
has not been set, all messages would be received by window.ove.socket.on
. These methods can be used to develop controllers that support interactive operations such as linking and brushing, across a number of different application instances or types.
Embedding OVE within an existing web application¶
Each OVE client
can be deployed in its own iFrame and embedded into an existing web application. This approach has been used in the Whiteboard App and the Replicator App. OVE also supports a number of useful properties that can be passed into the iFrame of each client
. The filters
property accepts an includeOnly
or exclude
child-property that can be used to specifically include or exclude sections from being displayed within a client
. Each OVE client
has a dark grey background, which can be set to none
using the transparentBackground
property. The load
property can be set to forcefully reload the contents of a client
.
These properties can be passed into all client
iFrames as a message sent to the core
application, as noted below:
window.ove.frame.send(
Constants.Frame.CHILD,
{
load: true,
transparentBackground: true,
filters: { includeOnly: [0, 1] }
},
'core');