Creating the StarPercent? function
Introduction
This document will walk through the creation of the
StarPercent function, which filters stars passing through it by retaining a certain percentage of the stars and discarding the rest.
Preliminaries
The
StarPercent Function will be in the "
filter" category, so the plugin will reside in the Java source file
viewcontrol/function/plugins/filter/StarPercent.java. If the
filter directory doesn't already exist, it must be created and a
Makefile placed in it, as follows:
include ../../../../Makefile.opts
The plugin will be defined by the
StarPercent.java file, which will start out looking like this:
package spiegel.viewcontrol.function.plugins.filter;
import spiegel.viewcontrol.function.Function;
public class StarPercent extends Function {
}
By virtue of extending the
Function class,
StarPercent is now a valid Function plugin. Spiegel will happily load it and create instances of it, though it doesn't do anything but look good as of yet. If you create an instance and click on it, the Plugin tab will show the name but no other useful information. Lets add some of that information. Here's the new version of the file:
package spiegel.viewcontrol.function.plugins.filter;
import spiegel.viewcontrol.function.Function;
public class StarPercent extends Function {
public static String displayName = "Extract percent of stars";
public static String description =
"Extracts a configurable percentage of stars from the input";
public static String author = "Tim Peterson";
}
With those three static variables added, the Plugin tab now displays the display name, description, and author, as well as using the display name in the function creation menu. This works because the
Function class examines itself when it is constructed, to determine what capabilities the subclass has. This is so that writing Functions can be as simple as possible, with no extra common code that has to be copied into every plugin. The
Function class will fill in the gaps by reading these static variables.
Inputs and Outputs
At the moment, our Function does nothing more than exist. It needs some inputs and outputs through which to receive and pass on the stars it's filtering. The data type used by the
Stars extractor is
StarIDMap, so that's what we'll use here. The magic in the
Function class allows us to declare the input and output with static variables, as follows:
public static DataInput.Info inputInfo =
new SingleDataInput.Info("stars", StarIDMap.class);
public static DataOutput.Info outputInfo =
new DataOutput.Info("stars", StarIDMap.class);
This specifies that there is an input named "
stars" which takes a single value of type
StarIDMap, and an output also named "
stars" which produces a single
StarIDMap. If we wanted to allow multiple connections or multiple data types, we could have used the
Info class belonging to a different
DataInput subclass.
But wait! These lines won't actually create the input and output; they just declare in the plugin's capabilities object that there will be such an input and output when it's created. The real magic occurs as soon as we add member variables to store them in:
private SingleDataInput<StarIDMap> starsIn;
private DataOutput starsOut;
When the
Function sees both a declared input named "
stars" and a member variable named "
starsIn" with type
DataInput, it automatically initializes the variable with an appropriate
DataInput object and adds it as an input to the Function. The object created is actually of type
SingleDataInput<StarIDMap> (as specified by the
Info object), which makes it easy to retrieve the value of the input given that we know it takes a single value of a single type. Beware, though: The type of the variable must match that of the
Info object used to declare it. The same type of magic happens between the declared output "
stars" and the "
starsOut" variable.
Now that we have inputs and outputs, we need to make something happen. Let's start by simply passing the data through unchanged. To do this, we need to override the
update() method so that we can update the output data whenever the input changes.
@Override
protected void update() {
starsOut.set(starsIn.get());
}
It's that simple. Just get the current data from the input and set it on the output. At this point the plugin should work properly (i.e. do nothing) when placed between a star visual and star extractor. Here's the full plugin thus far:
package spiegel.viewcontrol.function.plugins.filter;
import spiegel.viewcontrol.function.DataInput;
import spiegel.viewcontrol.function.DataOutput;
import spiegel.viewcontrol.function.Function;
import spiegel.viewcontrol.function.SingleDataInput;
import spiegel.viewcontrol.function.datatypes.StarIDMap;
public class StarPercent extends Function {
public static String displayName = "Extract percent of stars";
public static String description =
"Extracts a configurable percentage of stars from the input";
public static String author = "Tim Peterson";
public static DataInput.Info inputInfo =
new SingleDataInput.Info("stars", StarIDMap.class);
public static DataOutput.Info outputInfo =
new DataOutput.Info("stars", StarIDMap.class);
private SingleDataInput<StarIDMap> starsIn;
private DataOutput starsOut;
@Override
protected void update() {
starsOut.set(starsIn.get());
}
}
Adding Functionality
Now it's time to add some functionality. First, we need to tell it how many stars to keep and how many to remove. For this we need a parameter. Parameters are special inputs that have a default value, which is used when nothing is connected to the input, and facilities for setting the default value using commands. This allows the user to control the parameters manually but also be able to set up another Function to control it. Let's add the parameter:
public static DataInput.Info[] inputInfo = {
new ParamInput.Info<Double>("percent", new Double(100), Double.class),
new SingleDataInput.Info("stars", StarIDMap.class)
};
Note that while the
inputInfo variable used to contain a single object, it's now an array, yet it still works due to the magic of the
Function class. The same can be done with the
outputInfo field — either an object or an array of objects is accepted, to make the code look nicer when there's only one input or output. The declaration specifies that the parameter will be named "
percent," will supply
Double objects, and will have a default value of 100. Now for the parameter variable itself:
private ParamInput<Double> percentIn;
This will again be initialized automatically. Next we need to write the star-extraction algorithm. It will work by selecting the IDs of a bunch of stars, according to the
percent input. Each time the stars are updated, the stars with those IDs will be extracted and sent out the output. Here are some member variables for the algorithm:
private int numStars = 0;
private Set<Integer> keepStarIDs = new HashSet<Integer>();
The first variable,
numStars, holds the number of stars currently coming in on the input. We use this to determine if the number of stars changes (which will occur if a new data file is loaded). If it does, the set of stars to be kept needs to be updated. That set is stored in
keepStarIDs, which holds the
Integer IDs of the stars to be kept. The algorithm itself goes in the
update() method:
@Override
protected void update() {
First we'll get the full complement of stars from the input:
StarIDMap allStars = starsIn.get();
Then we'll determine if the set of stars to be kept needs updating. This is the case if the value of the
percent input has changed since the last update, or if the number of stars has changed. If so, we'll call
chooseStars() to update the set. In addition, we need to save the new number of stars for the next time around.
if(hasDataChanged(percentIn) || numStars != allStars.size()) {
keepStarIDs = chooseStars(allStars);
numStars = allStars.size();
}
Now we can extract the stars we've selected:
StarIDMap keepStars = extractStars(allStars);
Finally, we send the new stars out the output:
starsOut.set(keepStars);
}
Next we need the
chooseStars() method. This method takes the entire star map as its only argument and returns a set of star IDs selected from the input.
private HashSet<Integer> chooseStars(StarIDMap stars) {
Then we initialize the ID set and determine how many stars to keep by getting the value of the
percent parameter.
HashSet<Integer> ids = new HashSet<Integer>();
double keepPercent = percentIn.get();
int keepStars = (int) (stars.size() * keepPercent / 100.0);
Next, we go through the stars in the input, selecting the first
keepStars stars to keep. The
HashSet's iterator returns the stars in a fairly random order, so the resulting set of stars should be evenly selected from the population.
for(Integer id : stars.keySet()) {
ids.add(id);
If we've found enough stars, we break out of the loop.
if(ids.size() >= keepStars)
break;
}
Finally, we return the resulting set of IDs.
return ids;
}
One method remains:
extractStars(). Given a
StarIDMap?, it will extract the stars with the IDs chosen above and create a new map.
private StarIDMap extractStars(StarIDMap allStars) {
If all of the stars were selected (100%), then there's no need to create a new map. So, we can just return the input map.
if(allStars.size() == keepStarIDs.size()) {
return allStars;
Otherwise, we'll create a new map. We couldn't simply delete the unwanted stars from the input map, because that object must remain unchanged in case it is passed on to another Function.
} else {
StarIDMap keepStars = new StarIDMap();
We then pull out all of the chosen stars and put them in the map.
for(Integer id : keepStarIDs) {
keepStars.put(id, allStars.get(id));
}
Finally, we return the new map.
return keepStars;
}
}
The Function should now work properly. It is used by attaching a (properly-configured)
StarExtractor? to its input and a Star visual of some sort to its output. Initially the
percent parameter is set to 100, allowing all of the stars through. This can be changed by clicking on the Function, switching to the Command tab, entering a new value in the
set percent command, and pressing "Go."
This command was automatically generated by the
Function class when it set up the parameter input. But suppose we wanted to augment that command, for example by adding a slider to make it easy to select a percentage. What we need to do now is override that command with our own. This is done through the aptly-named
commands static variable (which again accepts an optional array). Into it we put strings describing the commands and what functions to call to handle them. If we select the
StarPercent function and look at the Object tab, one of the fields shows all of the command definitions for that Function. One of them reads, "
set percent <double value>." We'll copy that into the
commands variable and add a slider, like so:
public static String commands =
"set percent <double value text(3) slider(0, 100, 1)> { onSetParam }";
This creates a text box three characters wide and a slider ranging from 0 to 100 in increments of 1. The
onSetParam() method is used by all of the generated parameter commands, no matter what the data type is, so we can simply override the command and continue to use that method as long as we don't change the parameter format (one
double, in this case).
Here is the plugin in its entirety:
package spiegel.viewcontrol.function.plugins.filter;
import java.util.HashSet;
import java.util.Set;
import spiegel.viewcontrol.function.DataInput;
import spiegel.viewcontrol.function.DataOutput;
import spiegel.viewcontrol.function.Function;
import spiegel.viewcontrol.function.ParamInput;
import spiegel.viewcontrol.function.SingleDataInput;
import spiegel.viewcontrol.function.datatypes.StarIDMap;
public class StarPercent extends Function {
public static String displayName = "Extract percent of stars";
public static String description =
"Extracts a configurable percentage of stars from the input";
public static String author = "Tim Peterson";
public static DataInput.Info[] inputInfo = {
new ParamInput.Info<Double>("percent", new Double(100), Double.class),
new SingleDataInput.Info("stars", StarIDMap.class)
};
public static DataOutput.Info outputInfo =
new DataOutput.Info("stars", StarIDMap.class);
public static String commands =
"set percent <double value text(3) slider(0, 100, 1)> { onSetParam }";
private ParamInput<Double> percentIn;
private SingleDataInput<StarIDMap> starsIn;
private DataOutput starsOut;
private int numStars = 0;
private Set<Integer> keepStarIDs = new HashSet<Integer>();
@Override
protected void update() {
StarIDMap allStars = starsIn.get();
if(hasDataChanged(percentIn) || numStars != allStars.size()) {
keepStarIDs = chooseStars(allStars);
numStars = allStars.size();
}
StarIDMap keepStars = extractStars(allStars);
starsOut.set(keepStars);
}
private HashSet<Integer> chooseStars(StarIDMap stars) {
HashSet<Integer> ids = new HashSet<Integer>();
double keepPercent = percentIn.get();
int keepStars = (int) (stars.size() * keepPercent / 100.0);
for(Integer id : stars.keySet()) {
ids.add(id);
if(ids.size() >= keepStars)
break;
}
return ids;
}
private StarIDMap extractStars(StarIDMap allStars) {
if(allStars.size() == keepStarIDs.size()) {
// No changes necessary
return allStars;
} else {
// Extract the stars
StarIDMap keepStars = new StarIDMap();
for(Integer id : keepStarIDs) {
keepStars.put(id, allStars.get(id));
}
return keepStars;
}
}
}
--
TimPeterson - 12 Jun 2006