001/*
002 *                    BioJava development code
003 *
004 * This code may be freely disttributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be disttributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022
023package org.biojava.bio.gui;
024
025import java.awt.Color;
026import java.awt.Font;
027import java.awt.Graphics2D;
028import java.awt.Rectangle;
029import java.awt.Shape;
030import java.awt.font.FontRenderContext;
031import java.awt.font.GlyphVector;
032import java.awt.geom.AffineTransform;
033import java.awt.geom.Rectangle2D;
034import java.beans.PropertyChangeEvent;
035import java.beans.PropertyChangeListener;
036import java.beans.PropertyChangeSupport;
037import java.util.Comparator;
038import java.util.Iterator;
039import java.util.SortedSet;
040import java.util.TreeSet;
041
042import org.biojava.bio.BioError;
043import org.biojava.bio.BioException;
044import org.biojava.bio.BioRuntimeException;
045import org.biojava.bio.dist.Distribution;
046import org.biojava.bio.seq.io.SymbolTokenization;
047import org.biojava.bio.symbol.FiniteAlphabet;
048import org.biojava.bio.symbol.IllegalSymbolException;
049import org.biojava.bio.symbol.Symbol;
050
051/**
052 * A logo painter that paints in stacked letters.
053 * The total height of the letters is
054 * proportional to the total informaton in the state. The height of each letter
055 * is proportional to its emission probability. The most likely letter is drawn
056 * highest.
057 *
058 * @author Matthew Pocock
059 */
060public class TextLogoPainter implements LogoPainter {
061  /**
062   * A comparator to set up our letters & information scores nicely.
063   */
064  private static final Comparator COMP = new ResValComparator();
065
066  /**
067   * Supports the bean property logoFont.
068   */
069  private PropertyChangeSupport pcs;
070
071  /**
072   * The property for the logoFont.
073   */
074  private Font logoFont;
075
076  /**
077   * Retrieve the current font.
078   *
079   * @return the current logo font
080   */
081  public Font getLogoFont() {
082    return logoFont;
083  }
084
085  /**
086   * Set the current logo font.
087   *
088   * @param logoFont the new Font to render the logo letters in
089   */
090  public void setLogoFont(Font logoFont) {
091    firePropertyChange("logoFont", this.logoFont, logoFont);
092    this.logoFont = logoFont;
093  }
094
095  public void addPropertyChangeListener(PropertyChangeListener listener) {
096    pcs.addPropertyChangeListener(listener);
097  }
098
099  public void removePropertyChangeListener(PropertyChangeListener listener) {
100    pcs.removePropertyChangeListener(listener);
101  }
102
103  public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
104    pcs.addPropertyChangeListener(propertyName, listener);
105  }
106
107  public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
108    pcs.removePropertyChangeListener(propertyName, listener);
109  }
110
111  public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
112    pcs.firePropertyChange(propertyName, oldValue, newValue);
113  }
114
115  public void firePropertyChange(String propertyName, int oldValue, int newValue) {
116    pcs.firePropertyChange(propertyName, oldValue, newValue);
117  }
118
119  public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
120    pcs.firePropertyChange(propertyName, oldValue, newValue);
121  }
122
123  public void firePropertyChange(PropertyChangeEvent evt) {
124    pcs.firePropertyChange(evt);
125  }
126
127  public boolean hasListeners(String propertyName) {
128    return pcs.hasListeners(propertyName);
129  }
130
131  public void paintLogo(LogoContext ctxt) {
132    Graphics2D g2 = ctxt.getGraphics();
133    Distribution dist = ctxt.getDistribution();
134    SymbolStyle style = ctxt.getStyle();
135    SymbolTokenization toke = null;
136    try {
137        toke = dist.getAlphabet().getTokenization("token");
138    } catch (BioException ex) {
139        throw new BioRuntimeException(ex);
140    }
141
142    Rectangle bounds = ctxt.getBounds();
143    double width = bounds.getWidth();
144    double height = bounds.getHeight();
145    double base = bounds.getY() + bounds.getHeight();
146    
147    /* This used to have some built-in scaling support, but I've disabled this because
148     * DistributionLogo does scaling too!
149     */
150    
151    // double scale = height * (
152    //  DistributionLogo.totalInformation(dist) /
153    //  DistributionLogo.totalBits(dist)
154    // );
155
156    SortedSet info = new TreeSet(COMP);
157
158    try {
159      for(
160        Iterator i = ((FiniteAlphabet) dist.getAlphabet()).iterator();
161        i.hasNext();
162      ) {
163        Symbol s = (Symbol) i.next();
164        info.add(new ResVal(s, dist.getWeight(s) * height));
165      }
166    } catch (IllegalSymbolException ire) {
167      throw new BioError("Symbol distsapeared from dist alphabet", ire);
168    }
169
170    FontRenderContext frc = g2.getFontRenderContext();
171    for(Iterator i = info.iterator(); i.hasNext();) {
172      ResVal rv = (ResVal) i.next();
173
174      String s = null;
175      try {
176          s = toke.tokenizeSymbol(rv.getToken());
177      } catch (IllegalSymbolException ex) {
178          throw new BioRuntimeException(ex);
179      }
180      GlyphVector gv = logoFont.createGlyphVector(frc, s);
181      Shape outline = gv.getOutline();
182      Rectangle2D oBounds = outline.getBounds2D();
183
184      AffineTransform at = new AffineTransform();
185      at.setToTranslation(0.0, base-rv.getValue());
186      at.scale(width / oBounds.getWidth(), rv.getValue() / oBounds.getHeight());
187      at.translate(-oBounds.getMinX(), -oBounds.getMinY());
188      outline = at.createTransformedShape(outline);
189
190      try {
191        g2.setPaint(style.fillPaint(rv.getToken()));
192      } catch (IllegalSymbolException ire) {
193        g2.setPaint(Color.black);
194      }
195      g2.fill(outline);
196
197      try {
198        g2.setPaint(style.outlinePaint(rv.getToken()));
199      } catch (IllegalSymbolException ire) {
200        g2.setPaint(Color.gray);
201      }
202      g2.draw(outline);
203
204      base -= rv.getValue();
205    }
206  }
207
208  public TextLogoPainter() {
209    pcs = new PropertyChangeSupport(this);
210    logoFont = new Font("Tahoma", Font.PLAIN, 12);
211  }
212
213  /**
214   * A symbol/information tuple.
215   */
216  private static class ResVal {
217    private Symbol symbol;
218    private double value;
219
220    public final Symbol getToken() {
221      return symbol;
222    }
223
224    public final double getValue() {
225      return value;
226    }
227
228    public ResVal(Symbol sym, double val) {
229      symbol = sym;
230      value = val;
231    }
232  }
233
234  /**
235   * The comparator for comparing symbol/information tuples.
236   */
237  private static class ResValComparator implements Comparator {
238    public final int compare(Object o1, Object o2) {
239      ResVal rv1 = (ResVal) o1;
240      ResVal rv2 = (ResVal) o2;
241
242      double diff = rv1.getValue() - rv2.getValue();
243      if(diff < 0) return -1;
244      if(diff > 0) return +1;
245      return rv1.getToken().getName().compareTo(rv2.getToken().getName());
246    }
247  }
248}