001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed 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 */ 021package org.biojava.bio.gui.sequence; 022 023import java.awt.Color; 024import java.awt.FontMetrics; 025import java.awt.Graphics2D; 026import java.awt.event.MouseEvent; 027import java.awt.geom.AffineTransform; 028import java.awt.geom.Line2D; 029import java.util.List; 030 031import org.biojava.utils.AbstractChangeable; 032import org.biojava.utils.ChangeEvent; 033import org.biojava.utils.ChangeSupport; 034import org.biojava.utils.ChangeType; 035import org.biojava.utils.ChangeVetoException; 036 037 038/** 039 * OffsetRulerRenderer can render the ruler starting from an arbitrary offset from the sequence. 040 * For example if the Protein contained an N-Terminal His tag then coordinate 1 should correspond 041 * to the start of the protein, not the tag. This implementation borrows heavily from 042 * RulerRenderer 043 * 044 * @author Mark Southern 045 * 046 * @since 1.5 047 */ 048public class OffsetRulerRenderer extends AbstractChangeable implements SequenceRenderer { 049 public static final ChangeType OFFSET = new ChangeType("The ruler offset has changed", 050 "org.biojava.bio.gui.sequence.OffsetRulerRenderer", "OFFSET", 051 SequenceRenderContext.REPAINT 052 ); 053 public static final ChangeType TICKS = new ChangeType("The ruler tick direction has changed", 054 "org.biojava.bio.gui.sequence.OffsetRulerRenderer", "TICKS", 055 SequenceRenderContext.REPAINT 056 ); 057 public static final int TICKS_UP = 0; 058 public static final int TICKS_DOWN = 1; 059 private Line2D line; 060 private double depth; 061 private AffineTransform antiQuarter; 062 private int tickDirection; 063 private float tickHeight; 064 private float horizLabelOffset; 065 private float vertLabelOffset; 066 private int sequenceOffset = 0; 067 068 public OffsetRulerRenderer() throws IllegalArgumentException { 069 this(TICKS_DOWN, 0); 070 } 071 072 public OffsetRulerRenderer(int tickDirection, int sequenceOffset) 073 throws IllegalArgumentException { 074 this.sequenceOffset = sequenceOffset; 075 076 line = new Line2D.Double(); 077 antiQuarter = AffineTransform.getRotateInstance(Math.toRadians(-90)); 078 079 if ((tickDirection == TICKS_UP) || (tickDirection == TICKS_DOWN)) { 080 this.tickDirection = tickDirection; 081 } else { 082 throw new IllegalArgumentException( 083 "Tick direction may only be set to RulerRenderer.TICKS_UP or RulerRenderer.TICKS_DOWN" 084 ); 085 } 086 087 depth = 20.0; 088 tickHeight = 4.0f; 089 090 horizLabelOffset = (( float ) depth) - tickHeight - 2.0f; 091 vertLabelOffset = (( float ) depth) - ((tickHeight + 2.0f) * 2.0f); 092 } 093 094 public void setSequenceOffset(int offset) throws ChangeVetoException { 095 if (hasListeners()) { 096 ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT); 097 098 synchronized (cs) { 099 ChangeEvent ce = new ChangeEvent(this, OFFSET); 100 cs.firePreChangeEvent(ce); 101 this.sequenceOffset = offset; 102 cs.firePostChangeEvent(ce); 103 } 104 } else { 105 this.sequenceOffset = offset; 106 } 107 } 108 109 public int getSequenceOffset() { 110 return this.sequenceOffset; 111 } 112 113 public void setTickDirection(int dir) throws ChangeVetoException { 114 if (hasListeners()) { 115 ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT); 116 117 synchronized (cs) { 118 ChangeEvent ce = new ChangeEvent(this, TICKS); 119 cs.firePreChangeEvent(ce); 120 tickDirection = dir; 121 cs.firePostChangeEvent(ce); 122 } 123 } else { 124 tickDirection = dir; 125 } 126 } 127 128 public int getTickDirection() { 129 return tickDirection; 130 } 131 132 public double getMinimumLeader(SequenceRenderContext context) { 133 return 0.0; 134 } 135 136 public double getMinimumTrailer(SequenceRenderContext context) { 137 return 0.0; 138 } 139 140 public double getDepth(SequenceRenderContext src) { 141 return depth + 1.0; 142 } 143 144 public void paint(Graphics2D g2, SequenceRenderContext context) { 145 g2.setStroke(new java.awt.BasicStroke(1F)); 146 147 AffineTransform prevTransform = g2.getTransform(); 148 149 g2.setPaint(Color.black); 150 151 int min = context.getRange().getMin(); 152 int max = context.getRange().getMax(); 153 double minX = context.sequenceToGraphics(min); 154 double maxX = context.sequenceToGraphics(max); 155 double scale = context.getScale(); 156 157 double halfScale = scale * 0.5; 158 159 if (context.getDirection() == SequenceRenderContext.HORIZONTAL) { 160 if (tickDirection == TICKS_UP) { 161 line.setLine(minX - halfScale, depth, maxX + halfScale, depth); 162 } else { 163 line.setLine(minX - halfScale, 0.0, maxX + halfScale, 0.0); 164 } 165 } else { 166 if (tickDirection == TICKS_UP) { 167 line.setLine(depth, minX - halfScale, depth, maxX + halfScale); 168 } else { 169 line.setLine(0.0, minX - halfScale, 0.0, maxX + halfScale); 170 } 171 } 172 173 g2.draw(line); 174 175 FontMetrics fMetrics = g2.getFontMetrics(); 176 177 // The widest (== maxiumum) coordinate to draw 178 int coordWidth = fMetrics.stringWidth(Integer.toString(max)); 179 180 // Minimum gap getween ticks 181 double minGap = ( double ) Math.max(coordWidth, 40); 182 183 // How many symbols does a gap represent? 184 int realSymsPerGap = ( int ) Math.ceil(((minGap + 5.0) / context.getScale())); 185 186 // We need to snap to a value beginning 1, 2 or 5. 187 double exponent = Math.floor(Math.log(realSymsPerGap) / Math.log(10)); 188 double characteristic = realSymsPerGap / Math.pow(10.0, exponent); 189 190 int snapSymsPerGap; 191 192 if (characteristic > 5.0) { 193 // Use unit ticks 194 snapSymsPerGap = ( int ) Math.pow(10.0, exponent + 1.0); 195 } else if (characteristic > 2.0) { 196 // Use ticks of 5 197 snapSymsPerGap = ( int ) (5.0 * Math.pow(10.0, exponent)); 198 } else { 199 snapSymsPerGap = ( int ) (2.0 * Math.pow(10.0, exponent)); 200 } 201 202 min -= Math.abs(sequenceOffset); 203 max += Math.abs(sequenceOffset); 204 205 int minP = min + ((snapSymsPerGap - min) % snapSymsPerGap); 206 207 for (int index = minP; index <= max; index += snapSymsPerGap) { 208 double offset = context.sequenceToGraphics(index + sequenceOffset); 209 String labelString = String.valueOf(index); // + sequenceOffset); 210 float halfLabelWidth = fMetrics.stringWidth(labelString) / 2; 211 212 if (context.getDirection() == SequenceRenderContext.HORIZONTAL) { 213 if (tickDirection == TICKS_UP) { 214 line.setLine(offset + halfScale, depth - tickHeight, offset + halfScale, depth); 215 g2.drawString(labelString, ( float ) ((offset + halfScale) - halfLabelWidth), 216 horizLabelOffset 217 ); 218 } else { 219 line.setLine(offset + halfScale, 0.0, offset + halfScale, tickHeight); 220 g2.drawString(labelString, ( float ) ((offset + halfScale) - halfLabelWidth), 221 horizLabelOffset 222 ); 223 } 224 } else { // vertical 225 226 if (tickDirection == TICKS_UP) { 227 line.setLine(depth, offset + halfScale, depth - tickHeight, offset + halfScale); 228 g2.translate(vertLabelOffset, offset + halfScale + halfLabelWidth); 229 g2.transform(antiQuarter); 230 g2.drawString(labelString, 0.0f, 0.0f); 231 g2.setTransform(prevTransform); 232 } else { 233 line.setLine(0.0f, offset + halfScale, tickHeight, offset + halfScale); 234 g2.translate(vertLabelOffset, offset + halfScale + halfLabelWidth); 235 g2.transform(antiQuarter); 236 g2.drawString(labelString, 0.0f, 0.0f); 237 g2.setTransform(prevTransform); 238 } 239 } 240 241 g2.draw(line); 242 } 243 } 244 245 public SequenceViewerEvent processMouseEvent(SequenceRenderContext context, MouseEvent me, 246 List path 247 ) { 248 path.add(this); 249 250 int sPos = context.graphicsToSequence(me.getPoint()); 251 252 return new SequenceViewerEvent(this, null, sPos, me, path); 253 } 254}