import jalview.api.AlignmentColsCollectionI;
import jalview.api.AlignmentRowsCollectionI;
+import jalview.api.AlignmentViewPanel;
+import jalview.api.RendererListenerI;
import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.Annotation;
import jalview.datamodel.SequenceGroup;
import jalview.datamodel.SequenceI;
import jalview.renderer.seqfeatures.FeatureColourFinder;
-import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.util.Platform;
import jalview.viewmodel.OverviewDimensions;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.beans.PropertyChangeSupport;
+import java.util.BitSet;
+import java.util.Iterator;
+
+import javax.swing.Timer;
public class OverviewRenderer
{
// transparency of hidden cols/seqs overlay
private final float TRANSPARENCY = 0.5f;
+ public static final String UPDATE = "OverviewUpdate";
+
+ private static final int MAX_PROGRESS = 100;
+
+ final static int STATE_INIT = 0;
+
+ final static int STATE_NEXT = 1;
+
+ final static int STATE_DONE = 2;
+
+ private int state;
+
+ private Timer timer;
+
+ private boolean isJS = Platform.isJS();
+
+ private int delay = (isJS ? 1 : 0);
+
+ private int seqIndex;
+
+ private int pixelRow;
+
+ private Integer row;
+
+ private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
+ this);
+
private FeatureColourFinder finder;
// image to render on
private BufferedImage miniMe;
- // raw number of pixels to allocate to each column
+ /**
+ * Number of pixelsPerCol;
+ */
private float pixelsPerCol;
- // raw number of pixels to allocate to each row
+ /**
+ * Number of visible columns per pixel.
+ *
+ */
+ private float colsPerPixel;
+
+ /**
+ * raw number of pixels to allocate to each row
+ */
private float pixelsPerSeq;
+ /**
+ * true when colsPerPixel > 1
+ */
+ private boolean skippingColumns;
+
+ /**
+ * pre-calculated list of columns needed for a "dense" overview, where there
+ * are more columns than pixels
+ */
+
+ private int[] columnsToShow;
+
+ // height in pixels of graph
+ private int graphHeight;
+
// flag to indicate whether to halt drawing
private volatile boolean redraw = false;
private ResidueShaderI shader;
- private ResidueColourFinder resColFinder;
+ private OverviewResColourFinder resColFinder;
+
+ private boolean showProgress;
- public OverviewRenderer(FeatureRenderer fr, OverviewDimensions od,
- AlignmentI alignment,
- ResidueShaderI resshader)
+ private AlignmentViewPanel panel;
+
+ // private int sequencesHeight;
+
+ public OverviewRenderer(AlignmentViewPanel panel,
+ jalview.api.FeatureRenderer fr, OverviewDimensions od,
+ AlignmentI alignment, ResidueShaderI resshader,
+ OverviewResColourFinder colFinder)
{
- finder = new FeatureColourFinder(fr);
- resColFinder = new OverviewResColourFinder();
+ this(panel, fr, od, alignment, resshader, colFinder, true);
+ }
- al = alignment;
- shader = resshader;
+ /**
+ * @param panel
+ * @param fr
+ * @param od
+ * @param alignment
+ * @param resshader
+ * @param colFinder
+ * @param shwoProgress
+ * possibly not, in JavaScript and for testng
+ */
+ public OverviewRenderer(AlignmentViewPanel panel,
+ jalview.api.FeatureRenderer fr, OverviewDimensions od,
+ AlignmentI alignment, ResidueShaderI resshader,
+ OverviewResColourFinder colFinder, boolean showProgress)
+ {
+ {
+ this.panel = panel;
+ finder = new FeatureColourFinder(fr);
+ al = alignment;
+ shader = resshader;
+ resColFinder = colFinder;
+ this.showProgress = showProgress;
+
+ w = od.getWidth();
+ h = od.getHeight();
+ rows = od.getRows(alignment);
+ cols = od.getColumns(alignment);
+ graphHeight = od.getGraphHeight();
+ alignmentHeight = od.getSequencesHeight();
+
+ pixelsPerSeq = od.getPixelsPerSeq();
+ pixelsPerCol = od.getPixelsPerCol();
+ colsPerPixel = Math.max(1, 1f / pixelsPerCol);
+
+ skippingColumns = (pixelsPerCol < 1);
- pixelsPerCol = od.getPixelsPerCol();
- pixelsPerSeq = od.getPixelsPerSeq();
- miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
- BufferedImage.TYPE_INT_RGB);
+ }
}
/**
- * Draw alignment rows and columns onto an image
+ * Draw alignment rows and columns onto an image. This method is asynchronous
+ * in JavaScript and interruptible in Java.
+ *
+ * Whether hidden rows or columns are drawn depends upon the type of
+ * collection.
+ *
+ * Updated to skip through high-density sequences, where columns/pixels > 1.
*
- * @param rit
- * Iterator over rows to be drawn
- * @param cit
- * Iterator over columns to be drawn
+ * When the process is complete, the image is passed to the AlignmentViewPanel
+ * provided by the constructor.
+ *
+ * @param rows
+ * collection of rows to be drawn
+ * @param cols
+ * collection of columns to be drawn
* @return image containing the drawing
+ *
+ * @author Bob Hanson 2019.07.30
*/
- public BufferedImage draw(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols)
+ public void drawMiniMe()
+ {
+ state = STATE_INIT;
+ mainLoop();
+ }
+
+ protected void mainLoop()
+ {
+ out: while (!redraw)
+ {
+ switch (state)
+ {
+ case STATE_INIT:
+ init();
+ state = STATE_NEXT;
+ continue;
+ case STATE_NEXT:
+ if (!rowIterator.hasNext())
+ {
+ state = STATE_DONE;
+ continue;
+ }
+ nextRow();
+ if (!loop())
+ {
+ // Java
+ continue;
+ }
+ // JavaScript
+ return;
+ case STATE_DONE:
+ break out;
+ }
+ // Java will continue without a timeout
+ }
+ done();
+ }
+
+ private void init()
{
- int rgbcolor = Color.white.getRGB();
- int seqIndex = 0;
- int pixelRow = 0;
+ rowIterator = rows.iterator();
+ seqIndex = 0;
+ pixelRow = 0;
+ lastRowUpdate = 0;
+ lastUpdate = 0;
+ totalPixels = w * alignmentHeight;
+ if (showProgress)
+ {
+ changeSupport.firePropertyChange(UPDATE, -1, 0);
+ }
- for (int alignmentRow : rows)
+ miniMe = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
+ WritableRaster raster = miniMe.getRaster();
+ DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
+ pixels = db.getBankData()[0];
+ bscol = cols.getOverviewBitSet();
+ if (skippingColumns)
{
+ columnsToShow = calcColumnsToShow();
+ }
+
+ Platform.timeCheck(null, Platform.TIME_MARK);
+ }
+
+ private void nextRow()
+ {
+ row = rowIterator.next();
+ SequenceI seq = rows.getSequence(row);
+
+ // rate limiting step when rendering overview for lots of groups
+ SequenceGroup[] allGroups = al.findAllGroups(seq);
+
+ // calculate where this row extends to in pixels
+ int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq), h);
+ // this is the key modification -- we use bscol to jump to the next column
+ // when there are more columns than pixels.
+
+ for (int pixelCol = 0, colNext = 0, pixelEnd = 0, icol = bscol
+ .nextSetBit(0); icol >= 0; icol = getNextCol(icol, colNext))
+ {
+ // asynchronous exit flag
if (redraw)
{
break;
}
-
- // get details of this alignment row
- SequenceI seq = rows.getSequence(alignmentRow);
-
- // rate limiting step when rendering overview for lots of groups
- SequenceGroup[] allGroups = al.findAllGroups(seq);
-
- // calculate where this row extends to in pixels
- int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
- miniMe.getHeight() - 1);
-
- int colIndex = 0;
- int pixelCol = 0;
- for (int alignmentCol : cols)
+
+ ++colNext;
+ pixelEnd = getNextPixel(colNext, colNext);
+
+ if (pixelCol == pixelEnd)
{
- if (redraw)
- {
- break;
- }
-
- // calculate where this column extends to in pixels
- int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
- miniMe.getWidth() - 1);
-
- // don't do expensive colour determination if we're not going to use it
- // NB this is important to avoid performance issues in the overview
- // panel
- if (pixelCol <= endCol)
+ break;
+ }
+ else if (pixelCol < pixelEnd)
+ {
+ int rgb = getColumnColourFromSequence(allGroups, seq, icol);
+ // fill in the appropriate number of pixels
+ for (int row = pixelRow; row < endRow; ++row)
{
- rgbcolor = getColumnColourFromSequence(allGroups, seq,
- alignmentCol, finder);
-
- // fill in the appropriate number of pixels
- for (int row = pixelRow; row <= endRow; ++row)
+ for (int col = pixelCol; col < pixelEnd; ++col)
{
- for (int col = pixelCol; col <= endCol; ++col)
- {
- miniMe.setRGB(col, row, rgbcolor);
- }
+ // BH 2019.07.27 was:
+ //
+ // miniMe.setRGB(col, row, rgbcolor);
+ //
+ // but just directly writing to the int[] pixel buffer
+ // is three times faster by my experimentation
+ pixels[row * w + col] = rgb;
+ ndone++;
}
-
- pixelCol = endCol + 1;
}
- colIndex++;
+ pixelCol = pixelEnd;
+ // store last update value
+ if (showProgress)
+ {
+ lastUpdate = sendProgressUpdate(
+ pixelEnd * (endRow - 1 - pixelRow), totalPixels,
+ lastRowUpdate, lastUpdate);
+ }
}
- pixelRow = endRow + 1;
- seqIndex++;
+
}
+ if (pixelRow < endRow)
+ {
+ pixelRow = endRow;
+ // store row offset and last update value
+ if (showProgress)
+ {
+ // BH 2019.07.29 was (old) endRow + 1 (now endRow), but should be
+ // pixelRow + 1, surely
+ lastRowUpdate = sendProgressUpdate(endRow, alignmentHeight, 0,
+ lastUpdate);
+ lastUpdate = lastRowUpdate;
+ }
+ }
+ }
- overlayHiddenRegions(rows, cols);
- return miniMe;
+ /**
+ * Precalculate the columns that will be used for each pixel in a dense
+ * overview. So we have to skip through the bscol BitSet to pick up one
+ * (representative?) column for each pixel.
+ *
+ * Note that there is no easy solution if we want to do color averaging, but
+ * this method might be adapted to do that. Or it could be adapted to pick the
+ * "most representative color" for a group of columns.
+ *
+ * @author Bob Hanson 2019.09.03
+ * @return a -1 terminated int[]
+ */
+ private int[] calcColumnsToShow()
+ {
+ int[] a = new int[w + 1];
+ float colBuffer = 0;
+ float offset = bscol.nextSetBit(0);
+ if (offset < 0)
+ {
+ return new int[] { -1 };
+ }
+ int pixel = 0;
+ a[pixel++] = (int) offset;
+ // for example, say we have 10 pixels per column:
+ // ...............xxxxxxxx....xxxxxx.........xxxxxx......
+ // nextSet(i).....^...........^..............^...........
+ // nextClear..............^.........^..............^.....
+ // run lengths....|--n1--|....|-n2-|.........|-n3-|......
+ // 10 pixel/col...|---pixel1---||-----pixel2------|......
+ // pixel..........^0............^1.......................
+ for (int i, iClear = -1; pixel < w
+ && (i = bscol.nextSetBit(iClear + 1)) >= 0;)
+ {
+ // find the next clear bit
+ iClear = bscol.nextClearBit(i + 1);
+ // add the run length n1, n2, n3 to grow the column buffer
+ colBuffer += iClear - i; // n1, n2, etc.
+ // add columns if we have accumulated enough pixels
+
+ while (colBuffer > colsPerPixel && pixel < w)
+ {
+ colBuffer -= colsPerPixel;
+ offset += colsPerPixel;
+ a[pixel++] = i + (int) offset;
+ }
+ // set back column pointer relative to the next run
+ offset = -colBuffer;
+ }
+ // add a terminator
+ a[pixel] = -1;
+ return a;
}
/**
- * Find the colour of a sequence at a specified column position
+ * The next column is either a precalculated pixel (when there are multiple
+ * pixels per column) or the next set bit for the column that aligns with the
+ * next pixel (when there are more columns than pixels).
+ *
+ * When columns are hidden, this value is precalculated; otherwise it is
+ * calculated here.
+ *
+ * @param icol
+ * @param pixel
+ * pixel pointer into columnsToShow
+ * @return
+ */
+ private int getNextCol(int icol, int pixel)
+ {
+ return (skippingColumns ? columnsToShow[pixel]
+ : bscol.nextSetBit(icol + 1));
+ }
+
+ /**
+ * Derive the next pixel from either as the given pixel (when we are skipping
+ * columns because this is a dense overview and the pixel known), or from the
+ * current column based on pixels/column. The latter is used for drawing the
+ * hidden-column mask or for overviews that have more pixels across than
+ * columns.
+ *
+ * @param icol
+ * @param pixel
+ * @return
+ */
+ private int getNextPixel(int icol, int pixel)
+ {
+ return Math.min(skippingColumns && pixel > 0 ? pixel
+ : Math.round(icol * pixelsPerCol), w);
+ }
+
+ private ActionListener listener = new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mainLoop();
+ }
+
+ };
+
+ private boolean loop()
+ {
+ if (delay <= 0)
+ {
+ return false;
+ }
+ if (timer == null)
+ {
+ timer = new Timer(delay, listener);
+ timer.setRepeats(false);
+ timer.start();
+ }
+ else
+ {
+ timer.restart();
+ }
+ return true;
+ }
+
+ private void done()
+ {
+ if (!redraw)
+ {
+ Platform.timeCheck(
+ "overviewrender " + ndone + " pixels row:" + row + " redraw:"
+ + redraw,
+ Platform.TIME_MARK);
+ }
+
+ overlayHiddenRegions();
+ if (showProgress)
+ {
+ // final update to progress bar if present
+ if (redraw)
+ {
+ // aborted in Java
+ // BH was pixelRow - 1, but that could go negative
+ sendProgressUpdate(pixelRow, alignmentHeight, 0, 0);
+ }
+ else
+ {
+ // sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
+ sendProgressUpdate(1, 1, 0, 0);
+ }
+ }
+ panel.overviewDone(miniMe);
+ }
+
+ int ndone = 0;
+
+ private AlignmentRowsCollectionI rows;
+
+ private AlignmentColsCollectionI cols;
+
+ Iterator<Integer> rowIterator;
+
+ int alignmentHeight;
+
+ int totalPixels;
+
+ int lastRowUpdate;
+
+ int lastUpdate;
+
+ int[] pixels;
+
+ BitSet bscol;
+
+ int w, h;
+
+ /*
+ * Calculate progress update value and fire event
+ * @param rowOffset number of rows to offset calculation by
+ * @return new rowOffset - return value only to be used when at end of a row
+ */
+ private int sendProgressUpdate(int position, int maximum, int rowOffset,
+ int lastUpdate)
+ {
+ int newUpdate = rowOffset
+ + Math.round(MAX_PROGRESS * ((float) position / maximum));
+ if (newUpdate > lastUpdate)
+ {
+ changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
+ return newUpdate;
+ }
+ return newUpdate;
+ }
+
+ /*
+ * Find the RGB value of the colour of a sequence at a specified column position
*
* @param seq
* sequence to get colour for
* @param lastcol
* column position to get colour for
- * @param fcfinder
- * FeatureColourFinder to use
* @return colour of sequence at this position, as RGB
*/
- private int getColumnColourFromSequence(SequenceGroup[] allGroups,
- jalview.datamodel.SequenceI seq,
- int lastcol, FeatureColourFinder fcfinder)
+ int getColumnColourFromSequence(SequenceGroup[] allGroups, SequenceI seq,
+ int icol)
{
- Color color = Color.white;
-
- if ((seq != null) && (seq.getLength() > lastcol))
- {
- color = resColFinder.getResidueColour(true, shader, allGroups, seq,
- lastcol,
- fcfinder);
- }
-
- return color.getRGB();
+ return (seq == null || icol >= seq.getLength()
+ ? resColFinder.GAP_COLOUR
+ : resColFinder.getResidueColourInt(true, shader, allGroups, seq,
+ icol, finder));
}
/**
* Overlay the hidden regions on the overview image
*
- * @param rows
- * collection of rows the overview is built over
- * @param cols
- * collection of columns the overview is built over
*/
- private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols)
+ private void overlayHiddenRegions()
{
if (cols.hasHidden() || rows.hasHidden())
{
- BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
- miniMe.getHeight());
+ BufferedImage mask = buildHiddenImage();
Graphics2D g = (Graphics2D) miniMe.getGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
TRANSPARENCY));
g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
+ g.dispose();
}
}
* height of overview in pixels
* @return BufferedImage containing mask of hidden regions
*/
- private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
- AlignmentColsCollectionI cols, int width, int height)
+ private BufferedImage buildHiddenImage()
{
// new masking image
- BufferedImage hiddenImage = new BufferedImage(width, height,
+ BufferedImage hiddenImage = new BufferedImage(w, h,
BufferedImage.TYPE_INT_ARGB);
- int colIndex = 0;
- int pixelCol = 0;
-
- Color hidden = Color.DARK_GRAY.darker();
+ Color hidden = resColFinder.getHiddenColour();
Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
+ g2d.setColor(hidden);
// set background to transparent
- g2d.setComposite(AlphaComposite.Clear);
- g2d.fillRect(0, 0, width, height);
+ // g2d.setComposite(AlphaComposite.Clear);
+ // g2d.fillRect(0, 0, width, height);
// set next colour to opaque
g2d.setComposite(AlphaComposite.Src);
- for (int alignmentCol : cols)
+ // System.out.println(cols.getClass().getName());
+ if (cols.hasHidden())
{
- if (redraw)
- {
- break;
- }
-
- // calculate where this column extends to in pixels
- int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
- hiddenImage.getWidth() - 1);
-
- if (pixelCol <= endCol)
+ // AllColsCollection only
+ BitSet bs = cols.getHiddenBitSet();
+ for (int pixelCol = -1, icol2 = 0, icol = bs
+ .nextSetBit(0); icol >= 0; icol = bs.nextSetBit(icol2))
{
- // determine the colour based on the sequence and column position
- if (cols.isHidden(alignmentCol))
+ if (redraw)
{
- g2d.setColor(hidden);
- g2d.fillRect(pixelCol, 0, endCol - pixelCol + 1, height);
+ break;
+ }
+ icol2 = bs.nextClearBit(icol + 1);
+ int pixelEnd = getNextPixel(icol2, 0);
+ if (pixelEnd > pixelCol)
+ {
+ pixelCol = getNextPixel(icol, 0);
+ g2d.fillRect(pixelCol, 0, Math.max(1, pixelEnd - pixelCol),
+ h);
+ pixelCol = pixelEnd;
}
-
- pixelCol = endCol + 1;
}
- colIndex++;
-
}
-
- int seqIndex = 0;
- int pixelRow = 0;
- for (int alignmentRow : rows)
+ if (rows.hasHidden())
{
- if (redraw)
+ int seqIndex = 0;
+ int pixelRow = 0;
+ for (int alignmentRow : rows)
{
- break;
- }
+ if (redraw)
+ {
+ break;
+ }
- // calculate where this row extends to in pixels
- int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
- miniMe.getHeight() - 1);
+ // calculate where this row extends to in pixels
+ int endRow = Math.min(Math.round((++seqIndex) * pixelsPerSeq),
+ h);
- // get details of this alignment row
- if (rows.isHidden(alignmentRow))
- {
- g2d.setColor(hidden);
- g2d.fillRect(0, pixelRow, width, endRow - pixelRow + 1);
+ // get details of this alignment row
+ if (rows.isHidden(alignmentRow))
+ {
+ g2d.fillRect(0, pixelRow, w, endRow - 1 - pixelRow);
+ }
+ pixelRow = endRow;
}
- pixelRow = endRow + 1;
- seqIndex++;
}
-
+ g2d.dispose();
return hiddenImage;
}
/**
* Draw the alignment annotation in the overview panel
*
- * @param g
- * the graphics object to draw on
* @param anno
* alignment annotation information
- * @param charWidth
- * alignment character width value
- * @param y
- * y-position for the annotation graph
- * @param cols
- * the collection of columns used in the overview panel
*/
- public void drawGraph(Graphics g, AlignmentAnnotation anno, int charWidth,
- int y, AlignmentColsCollectionI cols)
+ public void drawGraph(AlignmentAnnotation anno)
{
+ int y = graphHeight;
+ Graphics g = miniMe.getGraphics();
+ g.translate(0, alignmentHeight);
+
Annotation[] annotations = anno.annotations;
+ float max = anno.graphMax;
g.setColor(Color.white);
- g.fillRect(0, 0, miniMe.getWidth(), y);
+ g.fillRect(0, 0, w, y);
- int height;
- int colIndex = 0;
- int pixelCol = 0;
- for (int alignmentCol : cols)
+ for (int pixelCol = 0, colNext = 0, pixelEnd = 0, len = annotations.length, icol = bscol
+ .nextSetBit(0); icol >= 0
+ && icol < len; icol = getNextCol(icol, colNext))
{
if (redraw)
{
+ if (showProgress)
+ {
+ changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
+ }
break;
}
- if (alignmentCol >= annotations.length)
+
+ ++colNext;
+ pixelEnd = getNextPixel(colNext, colNext);
+ Annotation ann = annotations[icol];
+ if (ann != null)
{
- break; // no more annotations to draw here
+ Color color = ann.colour;
+ g.setColor(color == null ? Color.black : color);
+ int height = Math.min(y, (int) ((ann.value / max) * y));
+ g.fillRect(pixelCol, y - height, Math.max(1, pixelEnd - pixelCol),
+ height);
}
- else
- {
- int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
- miniMe.getWidth() - 1);
-
- if (annotations[alignmentCol] != null)
- {
- if (annotations[alignmentCol].colour == null)
- {
- g.setColor(Color.black);
- }
- else
- {
- g.setColor(annotations[alignmentCol].colour);
- }
+ pixelCol = pixelEnd;
+ }
- height = (int) ((annotations[alignmentCol].value / anno.graphMax)
- * y);
- if (height > y)
- {
- height = y;
- }
+ g.translate(0, -alignmentHeight);
+ g.dispose();
- g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
- }
- pixelCol = endCol + 1;
- colIndex++;
- }
+ if (showProgress)
+ {
+ changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
+ MAX_PROGRESS);
}
+
}
/**
redraw = b;
}
}
+
+ public void addPropertyChangeListener(RendererListenerI listener)
+ {
+ changeSupport.addPropertyChangeListener(listener);
+ }
+
+ public void removePropertyChangeListener(RendererListenerI listener)
+ {
+ changeSupport.removePropertyChangeListener(listener);
+ }
}