-
Notifications
You must be signed in to change notification settings - Fork 18
Plugin Dev Guide
Datavyu plugins are extensions used to implement visualizations of datasets with temporal qualities (i.e. your dataset has a time domain). Datavyu ships with two such plugins: a video and audio plugin. The video plugin implements video playback through Quicktime while the audio plugin implements audio plugin through GStreamer.
The goal of the plugin system is to allow developers to create their own extensions for visualizing custom datasets. One real example of a custom non-trivial plugin is an Air Traffic Control plugin.
In this guide, I am going to walk you through creating a heart rate plugin. The heart rate plugin will read from a CSV file containing the patient's name, patient's heart rate, and timestamp (in seconds) associated with each heart rate. The CSV file will look something like this:
c
<patient name>
<timestamp 1>,<heart rate 1>
<timestamp 2>,<heart rate 2>
<timestamp 3>,<heart rate 3>
...
<timestamp n>,<heart rate n>
Here's the CSV file we will be using. We get a data point every second, for two hours. The data has been generated using Matlab.
The absolute minimum requirements for developing a plugin are:
- JDK 6
- Git: install a client of your choice, we need this so that we can get a copy of all relevant code
- Maven: for building Datavyu
I will assume that, like most Java developers, you are going to be using Eclipse. I would recommend installing the Sonatype Maven plugin.
Once you have the tools installed, here is how you get your environment development environment up and running.
-
First, clone the Datavyu source tree.
git clone https://github.com/Datavyu/datavyu.git
-
In Eclipse, import the Datavyu source tree through File -> Import -> Maven -> Existing Maven Projects. Select the Datavyu source tree folder and click finish. Wait a few moments as Maven sets up your project and downloads all dependencies. When done, your Eclipse project should look something like this. Verify that all is well by running the program (
org.datavyu.Datavyu
is the main entry point). -
Next, we would want to clone the Datavyu plugin library source tree.
git clone https://github.com/Datavyu/datavyu-lib.git
-
Import the plugin library source into Eclipse like you did for the Datavyu source tree.
That's it, the development environment is up and running.
We are now going to create an Eclipse project for our plugin.
-
In Eclipse, create a new Maven project through File -> New Project -> Maven -> Maven Project.
- Create a simple project, using the default workspace location or the location of your choice is ok. Next page.
- Group ID can be anything (I used
com.dteoh
). Artifact ID fill inheartrate
. Version0.1
. Leave everything else and press Finish.
-
At this point, you would have a new Eclipse project named
heartrate
. Because this is a Maven project, it would have created a bunch of predefined source locations for you. All our code would go into thesrc/main/java
source folder.pom.xml
in the project root folder is the Maven script for our project. -
Edit
pom.xml
. Update it to the following.<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.dteoh</groupId> <artifactId>heartrate</artifactId> <version>0.1</version> <repositories> <repository> <id>datavyu</id> <url>http://datavyu.org/maven2</url> <layout>default</layout> </repository> <repository> <id>maven</id> <url>http://repo1.maven.org/maven2</url> <layout>default</layout> </repository> </repositories> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.datavyu</groupId> <artifactId>datavyu-lib</artifactId> <version>0.0.58</version> </dependency> </dependencies>
-
What we have done is set up our Maven script to compile our code to be Java 1.6 compliant. We have also included a dependency on Datavyu's plugin library.
-
In Eclipse, check the project properties (right click on the project -> Properties). Under Java Build Path make sure that we have JDK 1.6 being used as a library. Under Java Compiler, do not enable project specific settings. Apply and OK.
We now have a base project for developing your plugin. The progress so far is tagged "base-project" in this guide's code repository.
We have created our base project, it is now time to create a "minimal" plugin. We are just going to implement the bare minimum for our code to be loaded by Datavyu as a plugin.
In our heartrate
project, create a top level package for our code to live in. This is usually whatever you have in the Maven group ID and artifact ID. I named my group ID com.dteoh
and artifact ID heartrate
so I am going to name my package com.dteoh.heartrate
. Datavyu doesn't actually care what you name your plugin or plugin packages, but it would be wise to choose something that doesn't conflict with the existing namespace used by Datavyu or any of its dependent libraries.
Now that we have our top level package, let's start by implementing the org.datavyu.plugins.Plugin
interface. Create a class HeartRatePlugin
which implements the Plugin
interface. We now have a bunch of interface methods to implement; most are fairly straight-forward to implement. From a design patterns perspective, Plugin
is similar to the factory pattern. The main goal of Plugin
is to create DataViewer
instances.
getClassifier
is used by Datavyu's plugin classification system. What the classification system does is group together plugins that achieve a similar goal. For example, the video plugins shipped with Datavyu are classified as datavyu.video
. The plugin system uses the classifier to search for similar plugins in the event that the preferred plugin cannot be found or loaded. An example of such a situation is when a Linux user tries to load a project file in Datavyu containing references to videos used by the Quicktime video plugin. Since Linux does not support Quicktime, Datavyu cannot load the Quicktime plugin. However, we have the datavyu.video
classifier. As it turns out, we have an experimental GStreamer video plugin that can be used to load the videos referenced in the project file.
@Override public String getClassifier() {
return "com.dteoh.heartrate";
}
If you don't know what classification to use for your plugin, don't worry about it. The safest approach is to just use the top-level package name you have chosen.
getFilters
returns an array of org.datavyu.plugins.Filter
s for describing the types of files that the plugin can open. Each array element denotes a separate group. For example, the video plugin is also able to open audio files so it returns two Filter
s: one for video files (mpeg, mp4, avi) and another for audio files (mp3, wav).
Since our heart rate plugin is only going to open CSV files, we only need one Filter
.
private static final Filter HR_FILTER = new Filter() {
List<String> exts = new ArrayList<String>();
FileFilter ff;
{
exts.add(".csv");
ff = new SuffixFileFilter(exts);
}
@Override public String getName() {
return "CSV files";
}
@Override public FileFilter getFileFilter() {
return ff;
}
@Override public Iterable<String> getExtensions() {
return exts;
}
};
private static final Filter[] FILTERS = new Filter[] { HR_FILTER };
@Override public Filter[] getFilters() {
return FILTERS;
}
SuffixFileFilter
is from the Apache Commons IO library.
getPluginName
returns the name of the plugin. Easy!
@Override public String getPluginName() {
return "Heart Rate Plugin";
}
getTypeIcon
can be used to return an icon for representing your plugin. Its dimensions should be 32x32 pixels. Return null
if you don't have one.
@Override public ImageIcon getTypeIcon() {
return null;
}
There are two more interface methods to implement, getNewDataViewer
and getViewerClass
, but we can't proceed until we implement another plugin interface. Create a class which implements the org.datavyu.plugins.DataViewer
interface. I named mine HRDataViewer
and placed it in the same package as our plugin class. We will stub this class out later.
getNewDataViewer
is what the plugin system invokes to create DataViewer
s; this is what visualizes the dataset on screen.
We haven't stubbed out our data viewer just yet so we are ignoring the parent
and modal
parameters. parent
is what our visualizer will need to "attach" to; it is the parent frame of our visualizer window. modal
is used to specify if our visualizer window is a modal frame.
@Override public DataViewer getNewDataViewer(final Frame parent, final boolean modal) {
return new HRDataViewer();
}
getViewerClass
returns the DataViewer
implementation class associated with the plugin.
@Override public Class<? extends DataViewer> getViewerClass() {
return HRDataViewer.class;
}
At this point, we are 99% done with this class. The only thing left to do is update the getNewDataViewer
method once we have stubbed out our data viewer.
Onto HRDataViewer
. Let's add a constructor to it.
/** Dialog for showing our visualizations. */
private JDialog hrDialog;
public HRDataViewer(final Frame parent, final boolean modal) {
Runnable edtTask = new Runnable() {
@Override public void run() {
hrDialog = new JDialog(parent, modal);
hrDialog.setName("HRDataViewer");
hrDialog.setResizable(true);
hrDialog.setSize(250, 250);
hrDialog.setVisible(true);
}
};
if (SwingUtilities.isEventDispatchThread()) {
edtTask.run();
} else {
SwingUtilities.invokeLater(edtTask);
}
}
Finish the getNewDataViewer
method in HeartRatePlugin
:
@Override public DataViewer getNewDataViewer(final Frame parent, final boolean modal) {
return new HRDataViewer(parent, modal);
}
Our constructor sets up an empty JDialog
. Since Swing objects need to be "used" from the AWT Event Dispatch Thread (EDT), we've got those if
statements for putting the creation in the right thread,
At the time of writing, Datavyu runs everything in the EDT. As explained in the [Java tutorials] (http://download.oracle.com/javase/tutorial/uiswing/concurrency/index.html), this really isn't a good idea because we can end up blocking the user interface. This is bad because the unresponsiveness of the program will cause the user to think that the program has crashed. The if
statement in our constructor is just there when (if) we patch this design problem so that plugins get created outside the EDT.
getParentJDialog
returns the dialog we are going to use to visualize data. It's the dialog that we created in our constructor.
@Override public JDialog getParentJDialog() {
return hrDialog;
}
getFrameRate
returns the frame rate of your visualizer. The unit is in frames per second. We will just stub this out for now to be 1 frame per second because our data file only has an update once every second.
@Override public float getFrameRate() {
return 1;
}
getDuration
returns the length (time-based) of the dataset being visualized in milliseconds. We will just stub this out for now to be 1 minute.
@Override public long getDuration() {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
}
setIdentifier
is used by Datavyu to identify data viewer instances. The identifier will be provided to your data viewer; you just need to store it somewhere.
/** Data viewer ID. */
private Identifier id;
@Override public void setIdentifier(final Identifier id) {
this.id = id;
}
getIdentifier
just needs to return the identifier given to your data viewer.
@Override public Identifier getIdentifier() {
return id;
}
setOffset
is used to set the playback offset, in milliseconds, of your dataset. You do not have to worry about any of the maths behind implementing
offsets in playback, Datavyu handles this for you. The only thing you need to do is store the offset somewhere.
/** Data viewer offset. */
private long offset;
@Override public void setOffset(final long offset) {
this.offset = offset;
}
getOffset
is used by Datavyu to retrieve any playback offsets, just return the value given to your data viewer.
@Override public long getOffset() {
return offset;
}
getTrackPainter
is used to retrieve any custom implementations of a TrackPainter
. TrackPainter
allows you to visualize data as it would appear on the tracks interface. [[Here is an example of the audio plugin's custom TrackPainter
|http://dteoh.github.com/datavyu-plugin-dev-guide/imgs/trackpainter.png]].
If you don't want to implement anything custom, just return a new DefaultTrackPainter
.
@Override public TrackPainter getTrackPainter() {
return new DefaultTrackPainter();
}
setDataViewerVisible
is used to toggle the visibility of our dialog window.
@Override public void setDataViewerVisible(final boolean isVisible) {
hrDialog.setVisible(isVisible);
}
We will stop implementing interface methods for now because we have the bare minimum for code to be recognized and launched as a plugin. We will now look at the easiest way of getting our plugin loaded by Datavyu.
Edit pom.xml
in the Datavyu main source tree. In the <dependencies>
section, we want to add our plugin project to it. This is why we made our plugin a Maven project. Add the plugin's metadata to Datavyu's dependencies like so:
<dependencies>
...
<dependency>
<groupId>com.dteoh</groupId>
<artifactId>heartrate</artifactId>
<version>0.1</version>
</dependency>
</dependencies>
You can of course find out what your plugin's dependency metadata is from the start of pom.xml
.
From Eclipse, just do a clean and build of Datavyu, then start Datavyu up. Bring up the data controller and press add data. If we have done things right, we should see our heart rate plugin in the plugins drop down menu like this. We should also see our file filter like this this. Let's just open any CSV file; we should get something like this.
Mission accomplished! We have a bare minimal plugin loaded by Datavyu. It doesn't do anything useful yet but we are now past the initial hurdle of plugin development. The code for this chapter is tagged "minimal-plugin".
So far, we have some code that Datavyu recognizes as a plugin. We have only implemented the bare minimum and it doesn't do anything yet. This chapter will focus on implementing reading from the data feed given to HRDataViewer
.
Our plugin will revolves around processing and displaying heart rate data. Let's make an abstraction for this data. Make a class named HRModel
. This class will handle creating a heart rate model from an input file. It will also provide other abstractions to the data, but we will implement these when needed.
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
public class HRModel {
/** The patient's name. */
private String patientName;
/** Time stamp associated with each heart rate data point. */
private List<Long> timestamps;
/** Heart rate data. */
private List<Double> heartRates;
public HRModel(final File data) {
timestamps = new ArrayList<Long>();
heartRates = new ArrayList<Double>();
parseData(data);
}
private void parseData(final File dataFile) {
LineIterator it = null;
try {
it = FileUtils.lineIterator(dataFile);
boolean firstLine = true;
while (it.hasNext()) {
String line = it.next();
if (firstLine) {
// Patient name.
patientName = line;
firstLine = false;
} else {
// Data point.
String[] data = line.split(",");
if (data.length == 2) {
long timestamp = parseTimestamp(data[0]);
double heartRate = parseHeartRate(data[1]);
timestamps.add(timestamp);
heartRates.add(heartRate);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
LineIterator.closeQuietly(it);
}
assert timestamps.size() == heartRates.size();
}
private long parseTimestamp(final String data) {
return TimeUnit.MILLISECONDS.convert(Long.parseLong(data),
TimeUnit.SECONDS);
}
private double parseHeartRate(final String data) {
return Double.parseDouble(data);
}
}
The model class for now has a constructor where you pass in a CSV data file. It will parse the file and populate our instance variables. Our timestamps from the file are stored in seconds; Datavyu currently measures time in milliseconds so it is easier for us to convert all our timestamps to milliseconds.
In HRDataViewer
let's flesh out the method that reads our input data feed. We will just set up some instance variables storing our data feed and heart rate model. We will also update the data viewer dialog's window title.
/** Data to visualize. */
private File data;
/** Data model. */
private HRModel model;
@Override public void setDataFeed(final File dataFeed) {
data = dataFeed;
model = new HRModel(data);
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
getParentJDialog().setTitle(dataFeed.getName());
}
});
}
The only thing that is missing from this method is error handling. At the time of writing, there is no way to signal to Datavyu that the input file is malformed; it is assumed that the data files that we get are all error free. Our heart rate model constructor handles errors by silently discarding erroneous lines.
For this method, just return the data file that the data viewer is currently visualizing.
@Override public File getDataFeed() {
return data;
}
Now that we are reading from input data, we can update the getDuration
method. Let's add a method to our model for calculating duration. In HRModel
:
public long getDuration() {
if (timestamps.isEmpty()) {
return 0;
}
return timestamps.get(timestamps.size() - 1) - timestamps.get(0);
}
Our model takes the difference between the last and first timestamp as the duration of the data.
Back at HRDataViewer
:
@Override public long getDuration() {
if (model == null) {
return 0;
}
return model.getDuration();
}
The duration is measured in milliseconds.
We are done with this chapter. If you run the plugin in Datavyu and open up the sample data file, you should see that our track is now two hours long. Our progress for this chapter is tagged "plugin-datafeed" in the code repository.
We need to be able to seek through our model data based on a timestamp. We also need to be able to get the current data point from our model. Let's update HRModel
to provide these abstractions.
We need to keep track where we currently are in our data structures. pos
is an index into list of timestamps and heart rates. Initialize pos = 0;
in the constructor.
/** Current position in our data structures. */
private int pos;
Based on pos
, we can retrieve the current heart rate and timestamp.
public double getCurrentHeartRate() {
if (pos < heartRates.size()) {
return heartRates.get(pos);
}
return 0;
}
public long getCurrentTimestamp() {
if (pos < timestamps.size()) {
return timestamps.get(pos);
}
return 0;
}
We need the ability to step through the model one-by-one, both backwards and forwards. We will use this later when we implement our animation clock.
public void next() {
pos = Math.min(pos + 1, timestamps.size() - 1);
}
public void prev() {
pos = Math.max(0, pos - 1);
}
Finally, we will implement seeking through our data structures based on a millisecond timestamp. To do this, we can just perform a binary search on our timestamps list.
public void seek(final long timestamp) {
int i = Collections.binarySearch(timestamps, timestamp);
if (i >= 0) {
pos = i;
} else {
// If it's not in the list, we get i = -(insertion point) - 1
// Find what the insertion point is, then take one away because
// we want the closest element.
pos = -(i + 1) - 1;
}
}
In HRDataViewer
, we can implement more interface methods. We can retrieve the current time from our model:
@Override public long getCurrentTime() throws Exception {
if (model == null) {
return 0;
}
return model.getCurrentTimestamp();
}
We can also implement seeking:
@Override public void seekTo(final long position) {
if (model != null) {
model.seek(position);
}
}
We are done with seeking through our data feed for now. The progress so far is tagged as "plugin-seeking" in the code repository.
Now that we have some way of accessing the data from our model, we can start building a (very simple) visualizer for the data. How you choose to visualize your data is up to you. For our plugin, we are going to keep it simple and stick with labels in a dialog box. Our visualizer should display the patient's name and current heart rate. We didn't put in a mechanism for retrieving the patient's name from the model, so let's put that in.
public String getPatientName() {
return patientName;
}
Next, we want to be able to listen to changes in the model. More specifically, we want to know when the current position in the model changed. We can add this to our model using property change listeners.
/** Handle property change propagation. */
private PropertyChangeSupport changeSupport;
public HRModel(final File data) {
...
changeSupport = new PropertyChangeSupport(this);
...
}
public void addPositionListener(final PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener("position", listener);
}
public void removePositionListener(final PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener("position", listener);
}
Next, we need to notify listeners whenever our position changes. Update all code that modifies pos
so that it also fires a property change event.
public void next() {
changePosition(Math.min(pos + 1, timestamps.size() - 1));
}
public void prev() {
changePosition(Math.max(0, pos - 1));
}
public void seek(final long timestamp) {
int i = Collections.binarySearch(timestamps, timestamp);
if (i >= 0) {
changePosition(i);
} else {
// If it's not in the list, we get i = -(insertion point) - 1
// Find what the insertion point is, then take one away because
// we want the closest element.
int newPos = -(i + 1) - 1;
changePosition(newPos);
}
}
private void changePosition(final int newIndex) {
int oldIndex = pos;
pos = newIndex;
changeSupport.firePropertyChange("position", oldIndex, pos);
}
Let's build a custom component for visualizing our data. The custom component will extend the JPanel
container and place our labels inside of it. It will also have some abstractions to it, like setting the model to use, listening to the model, updating from the model, and handle coloring ranges.
import java.awt.Color;
import java.awt.Font;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Formatter;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
public class HRPanel extends JPanel {
private JLabel patient;
private JLabel bpm;
private JLabel units;
private HRModel model;
private PropertyChangeListener positionListener;
public HRPanel() {
setLayout(new MigLayout());
setBackground(Color.GRAY.darker());
patient = new JLabel();
patient.setHorizontalAlignment(SwingConstants.CENTER);
patient.setFont(new Font("Helvetica", Font.PLAIN, 36));
add(patient, "span 2, growx, pushx, wrap");
bpm = new JLabel();
bpm.setHorizontalAlignment(SwingConstants.RIGHT);
bpm.setFont(new Font("Helvetica", Font.BOLD, 256));
add(bpm, "growx, pushx");
units = new JLabel();
units.setFont(new Font("Helvetica", Font.BOLD, 36));
units.setText("bpm");
add(units, "");
positionListener = new PropertyChangeListener() {
@Override public void propertyChange(
final PropertyChangeEvent evt) {
updateFromModel();
}
};
}
public void setModel(final HRModel model) {
if (this.model != null) {
this.model.removePositionListener(positionListener);
}
this.model = model;
model.addPositionListener(positionListener);
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
patient.setText(model.getPatientName());
}
});
updateFromModel();
}
private void updateFromModel() {
double heartRate = model.getCurrentHeartRate();
Formatter rateFormatter = new Formatter();
rateFormatter.format("%.1f", heartRate);
final String value = rateFormatter.toString();
final Color newColor = getColorForRate(heartRate);
SwingUtilities.invokeLater(new Runnable() {
@Override public void run() {
bpm.setText(value);
bpm.setForeground(newColor);
units.setForeground(newColor);
}
});
}
private Color getColorForRate(final double rate) {
if (rate < 80D) {
return Color.GREEN.brighter();
}
if (rate < 120D) {
return Color.YELLOW.darker();
}
return Color.RED.darker().darker();
}
}
Now that we have our custom component for visualization, we will need to put it into our data viewer dialog.
/** Panel for showing our visualizations. */
private HRPanel hrPanel;
public HRDataViewer(final Frame parent, final boolean modal) {
Runnable edtTask = new Runnable() {
@Override public void run() {
hrDialog = new JDialog(parent, modal);
hrDialog.setName("HRDataViewer");
hrDialog.setResizable(true);
hrDialog.setSize(640, 480);
Container c = hrDialog.getContentPane();
c.setLayout(new BorderLayout());
hrPanel = new HRPanel();
c.add(hrPanel, BorderLayout.CENTER);
hrDialog.setVisible(true);
}
};
if (SwingUtilities.isEventDispatchThread()) {
edtTask.run();
} else {
SwingUtilities.invokeLater(edtTask);
}
}
We should also update setDataFeed
so that our custom component is using the model.
@Override public void setDataFeed(final File dataFeed) {
...
hrPanel.setModel(model);
...
}
Getting excited yet? You should be! If you launch Datavyu and load the plugin up, you can see that our data is being displayed on screen, like this. You can drag the needle around, hit play, etc. and watch the plugin in action.
Building a simple plugin like this quickly is fairly easy, especially when the data rate and complexity is extremely low. We have managed to get away from implementing an animation clock, something that will need to be built when dealing with more complex data like video. As long as we use the right data structures and algorithms, we could probably absorb the performance penalty of Datavyu constantly issuing a seek request in favor of a simpler design.
Progress so far is tagged "plugin-vis" on the code repository.
To implement the rest of the DataViewer
interface relating to playback, we are going to need some kind of clock. In a more complex plugin, this clock will drive the animations in your visualizations according to the playback speed and direction. There are a few options for building a clock using the Java standard libraries. We could use the java.util.Timer
class, javax.swing.Timer
class, or the java.util.concurrent.ScheduledExecutorService
interface.
java.util.Timer
provides a general purpose timing mechanism. You can schedule the timer in units of milliseconds and it runs in its own thread. javax.swing.Timer
is similar to the java.util.Timer
class; the main difference is that javax.swing.Timer
runs in the EDT. java.util.concurrent.ScheduledExecutorService
is similar to java.util.Timer
. The most important difference, in my opinion, is that java.util.concurrent.ScheduledExecutorService
allows you to schedule tasks in units other than milliseconds. Another possible advantage is that you can return results from your periodic tasks.
I think building a clock using the java.util.concurrent.ScheduledExecutorService
is the best option. The reason for this is that Datavyu can set playback speeds of up to 32 times normal (1:1) speed. For extremely low data rates, using the other timing mechanisms is OK, but for something that is faster (such as video playback) millisecond resolution is not acceptable; you will end up with drift in playback. Using java.util.concurrent.ScheduledExecutorService
is not a guarantee against drift as there are other issues that affect the accuracy of the timing mechanism such as the resolution of timers provided by the operating system.
Here is our clock class. It provides the abstractions for running a periodic task and the calculations for the delay in-between tasks based on the framerate and playback speed. We can also check if the clock is ticking.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Clock {
private static final long ONE_SEC = TimeUnit.SECONDS.toNanos(1);
/** Our timing mechanism. */
private ScheduledExecutorService timer;
/** Is the clock running. */
private boolean ticking;
public Clock() {
timer = Executors.newSingleThreadScheduledExecutor();
ticking = false;
}
/**
* Start the clock and execute the given task periodically.
*
* @param task Task to execute.
* @param frameRate Framerate, measured in frames per second.
* @param speed Clock speed.
*/
public void start(final Runnable task, final double frameRate,
final double speed) {
if (isTicking()) {
stop();
}
// Calculate periodic delay.
long delay = (long) ((ONE_SEC / frameRate) / Math.abs(speed));
timer.scheduleWithFixedDelay(task, 0, delay, TimeUnit.NANOSECONDS);
ticking = true;
}
/**
* Stop the clock.
*/
public void stop() {
timer.shutdown();
ticking = false;
timer = Executors.newSingleThreadScheduledExecutor();
}
public boolean isTicking() {
return ticking;
}
}
Let's wire up HRDataViewer
to use the clock. Make a clock in the constructor:
/** Data viewer clock. */
private Clock clock;
public HRDataViewer(final Frame parent, final boolean modal) {
...
clock = new Clock();
}
This method returns true if our data viewer is playing back data. Since playback is related to the clock being running, that's what we will use.
@Override public boolean isPlaying() {
return clock.isTicking();
}
stop
just stops data viewer playback.
@Override public void stop() {
clock.stop();
}
This method sets the playback speed for our data viewer. It can be negative (backwards/rewind) or positive (forwards). We just need to store this value.
/** Data viewer current playback rate. */
private double playbackRate;
@Override public void setPlaybackSpeed(final float rate) {
playbackRate = rate;
}
This method is used to start visualization playback. We will need to handle the cases of backwards playback, forwards playback, and paused playback.
@Override public void play() {
Runnable task;
if (playbackRate < 0) {
task = new Runnable() {
@Override public void run() {
model.prev();
}
};
} else if (playbackRate > 0) {
task = new Runnable() {
@Override public void run() {
model.next();
}
};
} else {
stop();
return;
}
clock.start(task, getFrameRate(), playbackRate);
}
This is it for building our playback clock; the plugin no longer relies the Datavyu clock. If we have a heart monitor giving data at much more frequent intervals, all we will need to update is the getFrameRate
method such that it returns the new frequency.
Progress so far is tagged "plugin-clock" in the code repository.
If you are building a plugin where user settings for each DataViewer will need to be saved, then you can take advantage of the storeSettings
and loadSettings
methods. When a user saves their work to an Datavyu project file, data viewer settings will be stored in and loaded from the project file if these methods are implemented.
Here is how you can store the offset for HRDataViewer
using the java.util.Properties
class:
@Override public void storeSettings(final OutputStream os) {
Properties props = new Properties();
props.setProperty("offset", Long.toString(getOffset()));
try {
props.store(os, null);
} catch (IOException ex) {
ex.printStackTrace();
}
}
You can of course choose to serialize your data out using other formats such as XML or YAML.
Here is how you can load settings stored using Properties
:
@Override public void loadSettings(final InputStream is) {
Properties props = new Properties();
try {
props.load(is);
String property = props.getProperty("offset");
if ((property != null) && !"".equals(property)) {
setOffset(Long.parseLong(property));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
Actually, we don't have to write code for storing and restoring offsets. Since this functionality needs to be supported by all plugins, there are utility methods included in the Datavyu lib that we can use.
@Override public void storeSettings(final OutputStream os) {
try {
DataViewerUtils.storeDefaults(this, os);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override public void loadSettings(final InputStream is) {
try {
DataViewerUtils.loadDefaults(this, is);
} catch (IOException ex) {
ex.printStackTrace();
}
}
If you need to store custom information, have a look at the implementation of these utility methods to determine what needs to be serialized. At the time of writing, only the offsets are serialized.
Code for this chapter is tagged "plugin-settings".
If your plugin has custom user settings that need to be saved, there is a (crude) mechanism for signalling to Datavyu that something has changed and that the user should be prompted to save their work when the program shuts down. This mechanism is the ViewerStateListener
interface. Datavyu will register a listener to each DataViewer
instance. The listener can then be signalled to indicate change.
To add support for ViewerStateListener
in HRDataViewer
, modify the constructor such that we make a list to keep track of registered listeners.
/** Data viewer state listeners. */
private List<ViewerStateListener> stateListeners;
public HRDataViewer(final Frame parent, final boolean modal) {
...
stateListeners = new ArrayList<ViewerStateListener>();
}
This method is used to register a listener.
@Override public void addViewerStateListener(final ViewerStateListener vsl) {
if (vsl != null) {
stateListeners.add(vsl);
}
}
This method is used to remove a listener.
@Override public void removeViewerStateListener(final ViewerStateListener vsl) {
if (vsl != null) {
stateListeners.remove(vsl);
}
}
To signal a change in your data viewer, iterate through the registered state listeners and call the notifyStateChanged
method with null arguments.
for (ViewerStateListener listener : stateListeners) {
listener.notifyStateChanged(null, null);
}
The code for adding and removing state listeners is in the code repository and is tagged "plugin-listeners".
Plugins can define up to three action buttons for inclusion into the data viewer's track view header (circled in red). Each button is some implementation of Swing's AbstractButton
(such as JButton
) and is resized to be 16x16 pixels.
The CustomActions
interface is what allows you to define custom action buttons to be associated with each data viewer instance. There is also an extensible CustomActionsAdapter
class with default interface implementations.
For the heart rate plugin, let's just add a button to demonstrate how the interface is used. Define the button's variable and an implementation of CustomActions
:
/** Action button for demo purposes. */
private JButton sampleButton;
/** Supported custom actions. */
private CustomActions actions = new CustomActionsAdapter() {
@Override public AbstractButton getActionButton1() {
return sampleButton;
}
};
In our constructor, we will instantiate the button and its icon:
public HRDataViewer(final Frame parent, final boolean modal) {
Runnable edtTask = new Runnable() {
@Override public void run() {
...
sampleButton = new JButton();
sampleButton.setIcon(new ImageIcon(HRDataViewer.class.getResource("heart.png")));
sampleButton.setBorderPainted(false);
sampleButton.setContentAreaFilled(false);
}
};
...
}
heart.png
is a 16x16 pixels icon of a heart. Just search for one at Icon Finder. In Eclipse, make a package with the same name that encloses HRDataViewer
inside of src/main/resources
, then copy the icon into it.
Finally, we need to return our CustomActions
implementation in HRDataViewer
:
@Override public CustomActions getCustomActions() {
return actions;
}
If we run Datavyu (you may need to do a clean and build first) and load our heart rate plugin, we should see our custom action button.
Code for this chapter is tagged "plugin-actions".
At the time of writing, there is a very basic API for reading Datavyu's database. This API is available through the DataViewer
's setDatastore
interface method. After the data viewer is created, Datavyu will call this method and pass the data viewer an instance of Datastore
.
The Datastore
lets you retrieve a list of all variables (columns in the Datavyu spreadsheet) through the getAllVariables
method. Each variable is wrapped by the Variable
interface.
The Variable
interface allows you to:
- get all cells using
getCells
- get cells with temporal ordering applied using
getCellsTemporally
- get the name of the variable using
getName
Each cell is wrapped by the Cell
interface. The Cell
interface allows you to:
- get the cell offset timestamp in milliseconds using
getOffset
- get the cell onset timestamp in milliseconds using
getOnset
- get the cell value as a String using
getValue
You can provide a custom implementation of org.datavyu.views.component.TrackPainter
to visualize data over time as a complement to the instantaneous visualization of the data viewer. Subclass TrackPainter
and provide and implementation for paintCustom
. For those familiar with developing custom Swing components, paintCustom
behaves in the same manner as paintComponent
.
The painting order of a track is as follows:
- The carriage is painted.
-
paintCustom
is called for painting. - Track bookmarks are painted.
So in summary the carriage is at the bottom layer, your custom graphics are above the carriage, and the bookmarks are on top of everything else.
To assist the placement of pixels at the correct x-coordinate location with respect to the timeline, you can query the org.datavyu.models.component.ViewportState
interface. Here is how you can get an instance of the interface from a subclass of TrackPainter
:
ViewportState viewport = mixerModel.getViewportModel().getViewport();
To calculate the x-coordinate for a given millisecond timestamp (for example, 1000ms), you can write something like this:
double x = viewport.computePixelXOffset(1000);
The interface also includes a bunch of other helper methods, such as calculating timestamps from pixel x-coordinates. The Javadocs for the interface do a pretty good job at explaining how to use those methods.
Since data viewers can be closed or removed by users, there is a DataViewer
interface method called clearDataFeed
. Essentially what we want to do in this method is undo what was done in setDataFeed
. For some plugins, such as the audio plugin, this means shutting down event loops, stopping thread pools, and releasing buffers. The end result, if done correctly, is that we should be able to reuse the data viewer using a fresh invocation of setDataFeed
. At the time of writing, this functionality (reusing data viewers) has not been implemented. clearDataFeed
is currently being called when the user removes the data viewer.
To implement clearDataFeed
for the heart rate plugin, we will need to:
- make sure we have stopped playback
- de-register event listeners
- clear data buffers
Let's change our implementation of clearDataFeed
to take care of the first issue:
@Override public void clearDataFeed() {
stop();
}
We have event listeners in two places: the data viewer (ViewerStateListener
) and the HRModel
(PropertyChangeListener
). For the data viewer, Datavyu will automatically de-register itself so there is nothing else we have to do. For the model, we need to find out what listens to it and get those listeners to de-register themselves. In Eclipse, we can ask it to find what calls addPositionListener
defined in HRModel
. The only class that calls the method is HRPanel
's setModel
method. We will add a method to undo the registration in HRPanel
:
public void removeModel() {
if (model != null) {
model.removePositionListener(positionListener);
model = null;
}
}
Now we can call this method in clearDataFeed
:
@Override public void clearDataFeed() {
stop();
hrPanel.removeModel();
}
To clear data from our buffers, we need to add a clear method to HRModel
. The method should clear our data structures and reset the position in our buffer.
public void clearData() {
pos = 0;
timestamps.clear();
heartRates.clear();
}
Update the clearDataFeed
method:
@Override public void clearDataFeed() {
stop();
hrPanel.removeModel();
model.clearData();
model = null;
}
That's it for releasing resources. The code for this chapter is tagged "plugin-resources".
Up until now, the plugin has been loaded by Datavyu through a Maven dependency. To be able to distribute our plugin to other users, we need to compile our code into a JAR and then install the JAR in Datavyu's plugin directory.
To compile our plugin into a JAR, we can take advantage of the Maven Shade Plugin. This Maven plugin will compile our code and its dependencies into a single JAR. Update pom.xml
with the following <build>
:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>dist</shadedClassifierName>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
To compile the JAR, run the mvn package
command. The compiled JAR can be found at PLUGIN_ROOT/target/<artifact name>-dist.jar
. PLUGIN_ROOT
is the path to the root directory of the plugin project.
Installing the JAR involves copying the JAR to Datavyu's plugin directory.
On Windows, this directory is %USERPROFILE%\Application Data\NICTA\Datavyu\plugins\
.
On OSX, this directory is ~/Library/Application Support/Datavyu/plugins/
.
The updated build script in this chapter is available from the repository and is tagged "plugin-build-script".