From 0f4b0c151bdf8655456dc9f61b4f5df55c04c89f Mon Sep 17 00:00:00 2001 From: Mark Taylor Date: Thu, 12 Jun 2014 11:55:39 +0100 Subject: [PATCH] ttools: add example code for programmatic use of new STILTS plots A new class ttools.example.SinePlot runs an animated live plot in a window. It comes with a couple of associated source files that give two alternative ways to set the plot up. All commented in what I hope is a didactic fasion. --- .../ttools/example/ApiPlanePlotter.java | 164 ++++++++++++++ .../ttools/example/EnvPlanePlotter.java | 74 ++++++ .../ac/starlink/ttools/example/SinePlot.java | 212 ++++++++++++++++++ 3 files changed, 450 insertions(+) create mode 100644 ttools/src/main/uk/ac/starlink/ttools/example/ApiPlanePlotter.java create mode 100644 ttools/src/main/uk/ac/starlink/ttools/example/EnvPlanePlotter.java create mode 100644 ttools/src/main/uk/ac/starlink/ttools/example/SinePlot.java diff --git a/ttools/src/main/uk/ac/starlink/ttools/example/ApiPlanePlotter.java b/ttools/src/main/uk/ac/starlink/ttools/example/ApiPlanePlotter.java new file mode 100644 index 0000000000..cf83fdffce --- /dev/null +++ b/ttools/src/main/uk/ac/starlink/ttools/example/ApiPlanePlotter.java @@ -0,0 +1,164 @@ +package uk.ac.starlink.ttools.example; + +import java.awt.Color; +import java.awt.Dimension; +import java.io.IOException; +import javax.swing.Icon; +import javax.swing.JComponent; +import uk.ac.starlink.table.StarTable; +import uk.ac.starlink.ttools.plot.MarkShape; +import uk.ac.starlink.ttools.plot.Range; +import uk.ac.starlink.ttools.plot2.BasicCaptioner; +import uk.ac.starlink.ttools.plot2.Captioner; +import uk.ac.starlink.ttools.plot2.DataGeom; +import uk.ac.starlink.ttools.plot2.Navigator; +import uk.ac.starlink.ttools.plot2.PlotLayer; +import uk.ac.starlink.ttools.plot2.ShadeAxis; +import uk.ac.starlink.ttools.plot2.config.StyleKeys; +import uk.ac.starlink.ttools.plot2.data.DataSpec; +import uk.ac.starlink.ttools.plot2.data.DataStore; +import uk.ac.starlink.ttools.plot2.data.DataStoreFactory; +import uk.ac.starlink.ttools.plot2.data.SimpleDataStoreFactory; +import uk.ac.starlink.ttools.plot2.geom.PlaneAspect; +import uk.ac.starlink.ttools.plot2.geom.PlaneNavigator; +import uk.ac.starlink.ttools.plot2.geom.PlanePlotType; +import uk.ac.starlink.ttools.plot2.geom.PlaneSurfaceFactory; +import uk.ac.starlink.ttools.plot2.layer.MarkForm; +import uk.ac.starlink.ttools.plot2.layer.Outliner; +import uk.ac.starlink.ttools.plot2.layer.ShapeMode; +import uk.ac.starlink.ttools.plot2.layer.ShapePlotter; +import uk.ac.starlink.ttools.plot2.layer.ShapeStyle; +import uk.ac.starlink.ttools.plot2.layer.Stamper; +import uk.ac.starlink.ttools.plot2.paper.Compositor; +import uk.ac.starlink.ttools.plot2.task.ColumnDataSpec; +import uk.ac.starlink.ttools.plot2.task.PlotDisplay; + +/** + * PlanePlotter implementation that sets up a plot explicitly. + * There's a lot to do, it's quite complicated, even for a simple plot. + * If you don't like this approach, see the much more straightforward + * {@link EnvPlanePlotter} implementation instead. + * This approach however gives compile-time type-checking of the + * plot parameters. + * + * @author Mark Taylor + * @since 12 Jun 2014 + */ +public class ApiPlanePlotter implements SinePlot.PlanePlotter { + + public JComponent createPlotComponent( StarTable table, + boolean dataMayChange ) + throws InterruptedException, IOException { + + /* It's a 2d plot. */ + PlanePlotType plotType = PlanePlotType.getInstance(); + DataGeom geom = plotType.getPointDataGeoms()[ 0 ]; + + /* Create the Profile for the plot surface. This encapsulates + * those things about the geometry and appearance of the plot + * axes which will not change with window resizing, zooming etc. */ + PlaneSurfaceFactory surfFact = new PlaneSurfaceFactory(); + boolean xlog = false; + boolean ylog = false; + boolean xflip = false; + boolean yflip = false; + String xlabel = "X axis"; + String ylabel = "Y axis"; + Captioner captioner = new BasicCaptioner(); + double xyfactor = Double.NaN; + boolean grid = false; + double xcrowd = 1; + double ycrowd = 1; + boolean minor = true; + Color gridColor = Color.BLACK; + Color axlabelColor = Color.BLACK; + PlaneSurfaceFactory.Profile profile = + new PlaneSurfaceFactory.Profile( xlog, ylog, xflip, yflip, + xlabel, ylabel, captioner, + xyfactor, grid, xcrowd, ycrowd, + minor, gridColor, axlabelColor ); + + /* Set up a plot Aspect. This is the initial data range, + * and is subject to change by user navigation. */ + double[] xlimits = new double[] { 0, 1 }; + double[] ylimits = new double[] { -1.2, 1.2 }; + PlaneAspect aspect = new PlaneAspect( xlimits, ylimits ); + + /* Set up a Navigator which determines what mouse gestures are + * available to the user for plot pan/zoom etc. Note that + * anisotropic pan/zoom are available with wheel/drag gestures + * outside the plot axes. */ + double zoomFactor = StyleKeys.ZOOM_FACTOR.getDefaultValue(); + boolean xZoom = true; + boolean yZoom = true; + boolean xPan = true; + boolean yPan = true; + double xAnchor = Double.NaN; + double yAnchor = Double.NaN; + Navigator navigator = + new PlaneNavigator( zoomFactor, + xZoom, yZoom, xPan, yPan, xAnchor, yAnchor ); + + /* We will not use optional decorations for this plot. */ + Icon legend = null; + float[] legPos = null; + ShadeAxis shadeAxis = null; + Range shadeFixRange = null; + boolean surfaceAuxRange = false; + + /* Prepare the list of plot layers; in this case there is only one. */ + PlotLayer[] layers = { createScatterLayer( geom, table), }; + + /* Prepare the data cache. */ + int nl = layers.length; + DataSpec[] dataSpecs = new DataSpec[ nl ]; + for ( int il = 0; il < nl; il++ ) { + dataSpecs[ il ] = layers[ il ].getDataSpec(); + } + DataStoreFactory storeFact = new SimpleDataStoreFactory(); + DataStore dataStore = storeFact.readDataStore( dataSpecs, null ); + boolean caching = ! dataMayChange; + + /* Finally construct, size and return the plot component. */ + Compositor compositor = Compositor.SATURATION; + JComponent comp = + new PlotDisplay + ( plotType, layers, surfFact, profile, aspect, + legend, legPos, shadeAxis, shadeFixRange, + dataStore, surfaceAuxRange, navigator, + compositor, caching ); + comp.setPreferredSize( new Dimension( 500, 400 ) ); + return comp; + } + + /** + * Returns a plot layer plotting the first two columns of a given table + * against each other. + * + * @param geom data geom + * @param table data table + * @return new layer + */ + private PlotLayer createScatterLayer( DataGeom geom, StarTable table ) { + + /* Prepare the data for the scatter plot layer: use the first + * two columns of the supplied table as X and Y.*/ + DataSpec dataSpec = + new ColumnDataSpec( table, geom.getPosCoords(), + new int[][] { { 0 }, { 1 } } ); + + /* Prepare the graphical style of the scatter plot layer: + * it's a scatter plot with single-position markers, plotted + * in a single fixed colour. */ + ShapePlotter plotter = + ShapePlotter.createFlat2dPlotter( MarkForm.SINGLE ); + MarkShape shape = MarkShape.OPEN_CIRCLE; + int size = 2; + Outliner outliner = MarkForm.createMarkOutliner( shape, size ); + Stamper stamper = new ShapeMode.FlatStamper( Color.RED ); + ShapeStyle style = new ShapeStyle( outliner, stamper ); + + /* Combine the data and style to generate a plot layer. */ + return plotter.createLayer( geom, dataSpec, style ); + } +} diff --git a/ttools/src/main/uk/ac/starlink/ttools/example/EnvPlanePlotter.java b/ttools/src/main/uk/ac/starlink/ttools/example/EnvPlanePlotter.java new file mode 100644 index 0000000000..0f2f804ad1 --- /dev/null +++ b/ttools/src/main/uk/ac/starlink/ttools/example/EnvPlanePlotter.java @@ -0,0 +1,74 @@ +package uk.ac.starlink.ttools.example; + +import java.io.IOException; +import javax.swing.JComponent; +import uk.ac.starlink.table.StarTable; +import uk.ac.starlink.task.TaskException; +import uk.ac.starlink.ttools.plot2.task.Plot2Task; +import uk.ac.starlink.ttools.task.MapEnvironment; + +/** + * PlanePlotter implementation that uses the name/value pairs in the + * same way as the STILTS application command-line interface to set + * up a plot. + * + * This is much easier to do than the alternative, since the large + * majority of the options will assume sensible defaults if not set. + * It allows pretty much the same capabilities. However, it does + * not offer compile-time safety: there is no guarantee that a plot + * set up like this will not generate a run-time error. + * + * @author Mark Taylor + * @since 12 Jun 2014 + */ +public class EnvPlanePlotter implements SinePlot.PlanePlotter { + public JComponent createPlotComponent( StarTable table, + boolean dataMayChange ) + throws InterruptedException, IOException, TaskException { + + /* Create an execution environment for the stilts plot task. */ + MapEnvironment env = new MapEnvironment(); + + /* Populate the environment with parameter name/value pairs. + * For the available parameters and their values, see the user + * documentation of the corresponding STILTS command-line task. + * At time of writing, this documentation does not exist :-[. + * + * Some general points: + * + * - In most cases the values are strings. + * + * - For some parameters non-String objects of a relevant type + * are also allowed. In particular parameters accepting + * tables will take StarTable objects. + * + * - Most parameters are optional, and will assume sensible + * defaults if not set. There are several tens of parameters + * available, allowing detailed setup if you want to do it. + * In the example below, the required parameters are so marked, + * the others can be omitted if you want to accept default values. + */ + + /* Global parameters for the plot. */ + env.setValue( "type", "plane" ); // required + env.setValue( "insets", "10,30,30,8" ); + + /* Parameters for the first (in this case, only) layer; + * the parameter names have a trailing (arbitrary) label "1". + * The values of the x1/y1 parameters, giving the data coordinates, + * are names of the columns in the input table. */ + env.setValue( "layer1", "mark-flat" ); // required + env.setValue( "in1", table ); // required + env.setValue( "x1", "x" ); // required + env.setValue( "y1", "y" ); // required + env.setValue( "shape1", "open circle" ); + env.setValue( "size1", "2" ); + + /* You could add more layers here. */ + + /* Pass the populated environment to the Plot2Task object, + * which can turn it into a JComponent containing the plot. */ + boolean caching = ! dataMayChange; + return new Plot2Task().createPlotComponent( env, caching ); + } +} diff --git a/ttools/src/main/uk/ac/starlink/ttools/example/SinePlot.java b/ttools/src/main/uk/ac/starlink/ttools/example/SinePlot.java new file mode 100644 index 0000000000..580d1afa1f --- /dev/null +++ b/ttools/src/main/uk/ac/starlink/ttools/example/SinePlot.java @@ -0,0 +1,212 @@ +package uk.ac.starlink.ttools.example; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import uk.ac.starlink.table.ArrayColumn; +import uk.ac.starlink.table.ColumnStarTable; +import uk.ac.starlink.table.StarTable; + +/** + * Example programmatic use of stilts plot2 classes. + * This program plots a number of points near a sinusoidal curve. + * Optionally, the data can change and be replotted at time intervals. + * Either way, the plot is "live"; you can pan and zoom round it using + * the mouse. + * + *

Two alternative ways of setting up the plot are provided by way of + * example, but they have the same effect. + * + *

To use this class invoke the main() method from the command line. + * Use the -h flag for options. + * + * @author Mark Taylor + * @since 12 Jun 2014 + */ +public class SinePlot { + + private final int count_; + private final double[] xs_; + private final double[] ys_; + private final StarTable table_; + private PlanePlotter planePlotter_; + + /** + * Constructor. + * + * @param isEnv true to set up the plot using a MapEnvironment, + * false to do it using the detailed API + * @param count number of point to plot + */ + public SinePlot( boolean isEnv, int count ) throws Exception { + + /* Prepare an object which turns a table into a JComponent. + * There are two choices, one which uses the MapEnvironment + * and the other which uses the low-level API. Both produce + * just the same plot, it's a matter of taste which API you + * prefer. All the action (things you're likely to want to + * use as a template for your own plots) is in the implementation + * of these classes. */ + planePlotter_ = isEnv ? new EnvPlanePlotter() : new ApiPlanePlotter(); + + /* Set up a table containing data to plot, backed by a double[] array + * for each column. Changing the values in these arrays in place + * will change the content of the tables. */ + count_ = count; + xs_ = new double[ count ]; + ys_ = new double[ count ]; + ColumnStarTable table = ColumnStarTable.makeTableWithRows( count ); + table.addColumn( ArrayColumn.makeColumn( "x", xs_ ) ); + table.addColumn( ArrayColumn.makeColumn( "y", ys_ ) ); + table_ = table; + + /* Populate the table with some data. */ + updateTableData(); + } + + public void run( int updateMillis ) throws Exception { + + /* Determine whether we are going to be doing an animated plot + * or a static one. */ + boolean dataWillChange = updateMillis >= 0; + + /* This does the work of turning the table into a plot. */ + final JComponent plotComp = + planePlotter_.createPlotComponent( table_, dataWillChange ); + + /* If we are doing animation, set up a timer to change the table + * data in place every few milliseconds. For a static plot, + * you can ignore this. */ + if ( dataWillChange ) { + new Timer( true ).schedule( new TimerTask() { + public void run() { + updateTableData(); + SwingUtilities.invokeLater( new Runnable() { + public void run() { + plotComp.repaint(); + } + } ); + } + }, 0, updateMillis ); + } + + /* Post the plot component to the screen. */ + JFrame frame = new JFrame( SinePlot.class.getName() ); + frame.getContentPane().add( plotComp ); + frame.pack(); + frame.setVisible( true ); + } + + /** + * Populates the underlying data of the table with some noisy values. + */ + private void updateTableData() { + double rc = 1. / count_; + Random rnd = new Random(); + for ( int i = 0; i < count_; i++ ) { + double x = i * rc; + xs_[ i ] = x; + ys_[ i ] = Math.sin( 2 * Math.PI * x ) + rnd.nextGaussian() * 0.1; + } + } + + /** + * Abstracts the way that the table is turned into a 2d plot component. + */ + public interface PlanePlotter { + + /** + * Creates a JComponent holding a plot of the data in the first two + * columns of the supplied table. + * This plot is live, and can be resized (as required by Swing) + * and panned and zoomed (by various mouse drag and wheel gestures). + * + *

The dataMayChange parameter is used to determine + * whether the plot positions should be cached or not for repaints, + * either as required by Swing move/resize actions or + * as the result of the user navigating around the plot. + * It's always safe to set it true, but if the data is static, + * setting it false will give better performance. + * + * @param dataMayChange true if the table data may change during + * the lifetime of the plot + * @param table table to plot + */ + JComponent createPlotComponent( StarTable table, boolean dataMayChange ) + throws Exception; + } + + /** + * Main method. Use with -help. + */ + public static void main( String[] args ) throws Exception { + String usage = new StringBuffer() + .append( "\n " ) + .append( "Usage: " ) + .append( "\n " ) + .append( SinePlot.class.getName().replaceFirst( ".*\\.", "" ) ) + .append( " [-[no]move]" ) + .append( " [-[no]env]" ) + .append( " [-count npoint]" ) + .append( " [-verbose [-verbose]]" ) + .append( "\n" ) + .toString(); + List argList = new ArrayList( Arrays.asList( args ) ); + boolean move = true; + boolean env = false; + int count = 1000; + int verbLevel = 0; + for ( Iterator it = argList.iterator(); it.hasNext(); ) { + String arg = it.next(); + if ( arg.equals( "-move" ) ) { + it.remove(); + move = true; + } + else if ( arg.equals( "-nomove" ) ) { + it.remove(); + move = false; + } + else if ( arg.equals( "-env" ) ) { + it.remove(); + env = true; + } + else if ( arg.equals( "-noenv" ) ) { + it.remove(); + env = false; + } + else if ( arg.equals( "-count" ) ) { + it.remove(); + count = Integer.parseInt( it.next() ); + it.remove(); + } + else if ( arg.equals( "-verbose" ) ) { + it.remove(); + verbLevel++; + } + else if ( arg.startsWith( "-h" ) ) { + it.remove(); + System.out.println( usage ); + return; + } + } + if ( argList.size() > 0 ) { + System.err.println( usage ); + System.exit( 1 ); + } + Logger.getLogger( "uk.ac.starlink.ttools" ) + .setLevel( new Level[] { Level.WARNING, + Level.INFO, + Level.CONFIG }[ verbLevel ] ); + int updateMillis = move ? 100 : -1; + new SinePlot( env, count ).run( updateMillis ); + } +}