View Javadoc

1   /*--------------------------------------------------------------------------
2    *  Copyright 2011 utgenome.org
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *--------------------------------------------------------------------------*/
16  //--------------------------------------
17  // utgb-core Project
18  //
19  // ReadCanvas.java
20  // Since: 2011/01/06
21  //
22  //--------------------------------------
23  package org.utgenome.graphics;
24  
25  import java.awt.BasicStroke;
26  import java.awt.Color;
27  import java.awt.Font;
28  import java.awt.FontMetrics;
29  import java.awt.Graphics2D;
30  import java.awt.RenderingHints;
31  import java.awt.geom.AffineTransform;
32  import java.awt.image.BufferedImage;
33  import java.io.File;
34  import java.io.IOException;
35  import java.io.OutputStream;
36  import java.util.ArrayList;
37  import java.util.HashMap;
38  import java.util.List;
39  
40  import javax.imageio.ImageIO;
41  
42  import org.utgenome.gwt.utgb.client.UTGBClientException;
43  import org.utgenome.gwt.utgb.client.bio.CIGAR;
44  import org.utgenome.gwt.utgb.client.bio.Gap;
45  import org.utgenome.gwt.utgb.client.bio.Interval;
46  import org.utgenome.gwt.utgb.client.bio.OnGenome;
47  import org.utgenome.gwt.utgb.client.bio.OnGenomeDataVisitorBase;
48  import org.utgenome.gwt.utgb.client.bio.SAMReadLight;
49  import org.utgenome.gwt.utgb.client.bio.SAMReadPair;
50  import org.utgenome.gwt.utgb.client.bio.SAMReadPairFragment;
51  import org.utgenome.gwt.utgb.client.canvas.IntervalLayout;
52  import org.utgenome.gwt.utgb.client.canvas.IntervalLayout.LocusLayout;
53  import org.utgenome.gwt.utgb.client.canvas.PrioritySearchTree.Visitor;
54  import org.utgenome.gwt.utgb.client.track.TrackWindow;
55  import org.utgenome.gwt.utgb.server.util.graphic.GraphicUtil;
56  import org.xerial.util.log.Logger;
57  
58  /**
59   * For generating {@link BufferedImage} instance of a read layout
60   * 
61   * @author leo
62   * 
63   */
64  public class ReadCanvas {
65  
66  	private static Logger _logger = Logger.getLogger(ReadCanvas.class);
67  	private final GenomeWindow window;
68  	private BufferedImage image;
69  	private Graphics2D g;
70  
71  	private IntervalLayout layout = new IntervalLayout();
72  
73  	public static class DrawStyle {
74  		public int geneHeight = 2;
75  		public int geneMargin = 1;
76  		public boolean overlapPairedReads = true;
77  		public boolean showStrand = true;
78  		public boolean drawShadow = true;
79  		public int fontWidth = 10;
80  		public float clippedRegionAlpha = 0.2f;
81  
82  		public Color COLOR_GAP = new Color(0x66, 0x66, 0x66);
83  		public Color COLOR_PADDING = new Color(0x33, 0x33, 0x66);
84  		public Color COLOR_SHADOW = new Color(30, 30, 30, (int) (255 * 0.6f));
85  
86  		public Color COLOR_READ_DEFAULT = new Color(0xCC, 0xCC, 0xCC);
87  		public Color COLOR_FORWARD_STRAND = new Color(0xd8, 0x00, 0x67);
88  		public Color COLOR_REVERSE_STRAND = new Color(0x00, 0x67, 0xd8);
89  
90  		public Color COLOR_WIRED_READ_F = new Color(0xff, 0x99, 0x66);
91  		public Color COLOR_WIRED_READ_R = new Color(0x66, 0x99, 0xff);
92  		public Color COLOR_ORPHAN_READ_F = new Color(0xff, 0x66, 0x99);
93  		public Color COLOR_ORPHAN_READ_R = new Color(0x66, 0x99, 0xff);
94  
95  		private HashMap<Character, Color> colorTable = new HashMap<Character, Color>();
96  
97  		private String colorA = "50B6E8";
98  		private String colorC = "E7846E";
99  		private String colorG = "84AB51";
100 		private String colorT = "FFA930";
101 		public String colorN = "EEEEEE";
102 		public int repeatColorAlpha = 50;
103 
104 		{
105 			colorTable.put('a', GraphicUtil.parseColor(colorA, repeatColorAlpha));
106 			colorTable.put('c', GraphicUtil.parseColor(colorC, repeatColorAlpha));
107 			colorTable.put('g', GraphicUtil.parseColor(colorG, repeatColorAlpha));
108 			colorTable.put('t', GraphicUtil.parseColor(colorT, repeatColorAlpha));
109 			colorTable.put('n', GraphicUtil.parseColor(colorN, repeatColorAlpha));
110 			colorTable.put('A', GraphicUtil.parseColor(colorA));
111 			colorTable.put('C', GraphicUtil.parseColor(colorC));
112 			colorTable.put('G', GraphicUtil.parseColor(colorG));
113 			colorTable.put('T', GraphicUtil.parseColor(colorT));
114 			colorTable.put('N', GraphicUtil.parseColor(colorN));
115 		}
116 
117 		public void setBaseColor(char base, Color c) {
118 			colorTable.put(Character.toUpperCase(base), c);
119 			colorTable.put(Character.toLowerCase(base), new Color(c.getRed(), c.getGreen(), c.getBlue(), repeatColorAlpha));
120 		}
121 
122 		public Color getBaseColor(char base) {
123 			if (colorTable.containsKey(base))
124 				return colorTable.get(base);
125 			else
126 				return Color.white;
127 		}
128 
129 		public Color getReadColor(OnGenome g) {
130 
131 			if (SAMReadLight.class.isAssignableFrom(g.getClass())) {
132 				return getSAMReadColor(SAMReadLight.class.cast(g));
133 			}
134 			return getReadColor_internal(g);
135 		}
136 
137 		private Color getReadColor_internal(OnGenome g) {
138 			if (showStrand) {
139 				if (g instanceof Interval) {
140 					Interval r = (Interval) g;
141 					return r.isSense() ? COLOR_FORWARD_STRAND : COLOR_REVERSE_STRAND;
142 				}
143 			}
144 			return COLOR_READ_DEFAULT;
145 		}
146 
147 		public Color getClippedReadColor(OnGenome g) {
148 			Color c = getReadColor(g);
149 			return new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (255 * clippedRegionAlpha));
150 		}
151 
152 		private Color getSAMReadColor(SAMReadLight r) {
153 			if (r.isPairedRead()) {
154 				if (r.isMappedInProperPair())
155 					return getReadColor_internal(r);
156 				else
157 					return r.isSense() ? COLOR_WIRED_READ_F : COLOR_WIRED_READ_R;
158 			}
159 			else {
160 				return r.isSense() ? COLOR_ORPHAN_READ_F : COLOR_ORPHAN_READ_R;
161 			}
162 		}
163 
164 	}
165 
166 	private DrawStyle style;
167 
168 	public ReadCanvas(int width, int height, GenomeWindow window) {
169 		this(width, height, window, new DrawStyle());
170 	}
171 
172 	public ReadCanvas(int width, int height, GenomeWindow window, DrawStyle style) {
173 		this.window = window;
174 		this.style = style;
175 		setPixelSize(width, height);
176 	}
177 
178 	public void setPixelSize(int width, int height) {
179 		image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
180 		g = image.createGraphics();
181 	}
182 
183 	public Graphics2D getGraphics() {
184 		return g;
185 	}
186 
187 	public void toPNG(OutputStream out) throws IOException {
188 		ImageIO.write(image, "png", out);
189 	}
190 
191 	public void toPNG(File out) throws IOException {
192 		ImageIO.write(image, "png", out);
193 	}
194 
195 	public void draw(List<OnGenome> dataSet) {
196 		layout.setAllowOverlapPairedReads(style.overlapPairedReads);
197 		layout.setKeepSpaceForLabels(false);
198 		layout.setTrackWindow(new TrackWindow(getPixelWidth(), (int) window.startIndexOnGenome, (int) window.endIndexOnGenome));
199 
200 		int maxOffset = layout.reset(dataSet, style.geneHeight);
201 		final int h = style.geneHeight + style.geneMargin;
202 		final int canvasHeight = (maxOffset + 1) * h;
203 		setPixelSize(image.getWidth(), canvasHeight);
204 
205 		final ReadPainter painter = new ReadPainter();
206 		layout.depthFirstSearch(new Visitor<IntervalLayout.LocusLayout>() {
207 			public void visit(LocusLayout layout) {
208 				painter.setLocusLayout(layout);
209 				layout.getLocus().accept(painter);
210 			}
211 		});
212 	}
213 
214 	private int getReadHeight() {
215 		return style.geneHeight + style.geneMargin;
216 	}
217 
218 	private class ReadPainter extends OnGenomeDataVisitorBase {
219 		private LocusLayout currentLayout;
220 
221 		public void setLocusLayout(LocusLayout layout) {
222 			this.currentLayout = layout;
223 		}
224 
225 		public int getYPos() {
226 			return currentLayout.scaledHeight(getReadHeight());
227 		}
228 
229 		public int getYPos(int y) {
230 			return LocusLayout.scaledHeight(y, getReadHeight());
231 		}
232 
233 		@Override
234 		public void visitSAMReadPair(SAMReadPair pair) {
235 
236 			SAMReadLight first = pair.getFirst();
237 			SAMReadLight second = pair.getSecond();
238 
239 			int y1 = getYPos();
240 			int y2 = y1;
241 
242 			if (!style.overlapPairedReads && first.unclippedSequenceHasOverlapWith(second)) {
243 				if (first.unclippedStart > second.unclippedStart) {
244 					SAMReadLight tmp = first;
245 					first = second;
246 					second = tmp;
247 				}
248 				y2 = getYPos(currentLayout.getYOffset() + 1);
249 			}
250 			else {
251 				visitGap(pair.getGap());
252 			}
253 
254 			drawSAMRead(first, y1);
255 			drawSAMRead(second, y2);
256 		}
257 
258 		@Override
259 		public void visitSAMReadLight(SAMReadLight r) {
260 			drawSAMRead(r, getYPos());
261 		}
262 
263 		@Override
264 		public void visitSAMReadPairFragment(SAMReadPairFragment fragment) {
265 			visitGap(fragment.getGap());
266 			drawSAMRead(fragment.oneEnd, getYPos());
267 		}
268 
269 		@Override
270 		public void visitGap(Gap p) {
271 			drawPadding(p.getStart(), p.getEnd(), getYPos(), style.COLOR_GAP);
272 		}
273 	}
274 
275 	private int pixelPositionOnCanvas(int indexOnGenome) {
276 		return window.pixelPositionOnWindow(indexOnGenome, image.getWidth());
277 	}
278 
279 	public void drawRegion(OnGenome region, int y) {
280 		drawGeneRect(region.getStart(), region.getEnd(), y, style.getReadColor(region));
281 	}
282 
283 	public void drawRegion(int startOnGenome, int endOnGenome, int y, Color c, boolean drawShadow) {
284 		int x1 = pixelPositionOnCanvas(startOnGenome);
285 		int x2 = pixelPositionOnCanvas(endOnGenome);
286 
287 		int boxWidth = x2 - x1;
288 		if (boxWidth <= 0)
289 			boxWidth = 1;
290 
291 		AffineTransform saved = g.getTransform();
292 		g.translate(x1, y);
293 		g.setColor(c);
294 		g.fillRect(0, 0, boxWidth, style.geneHeight);
295 		g.setTransform(saved);
296 
297 		if (_logger.isTraceEnabled())
298 			_logger.trace(String.format("-gene rect - x:%d, y:%d, width:%d, height:%d, color:%s", x1, y, boxWidth, style.geneHeight, c.toString()));
299 
300 		if (drawShadow) {
301 			g.setColor(style.COLOR_SHADOW);
302 			//g.setStroke(new BasicStroke(1f));
303 
304 			saved = g.getTransform();
305 			g.translate(x1, y);
306 			g.drawLine(1, style.geneHeight, boxWidth, style.geneHeight);
307 			g.drawLine(boxWidth, style.geneHeight, boxWidth, 0);
308 			g.setTransform(saved);
309 		}
310 
311 	}
312 
313 	public void drawGeneRect(int startOnGenome, int endOnGenome, int y, Color c) {
314 
315 		drawRegion(startOnGenome, endOnGenome, y, c, style.drawShadow);
316 
317 	}
318 
319 	public void drawPadding(int startOnGenome, int endOnGenome, int y, Color c) {
320 
321 		g.setColor(c);
322 		g.setStroke(new BasicStroke(0.5f));
323 		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
324 		int yPos = (int) (y + (style.geneHeight / 2) + 0.5f);
325 		g.drawLine((int) (pixelPositionOnCanvas(startOnGenome) + 0.5f), yPos, (int) (pixelPositionOnCanvas(endOnGenome) + 0.5f), yPos);
326 	}
327 
328 	private static class PostponedInsertion {
329 		final int start;
330 		final String subseq;
331 
332 		public PostponedInsertion(int start, String subseq) {
333 			this.start = start;
334 			this.subseq = subseq;
335 		}
336 
337 	}
338 
339 	public void drawSAMRead(SAMReadLight r, int y) {
340 
341 		try {
342 			int cx1 = pixelPositionOnCanvas(r.unclippedStart);
343 			int cx2 = pixelPositionOnCanvas(r.unclippedEnd);
344 
345 			int gx1 = pixelPositionOnCanvas(r.getStart());
346 			int gx2 = pixelPositionOnCanvas(r.getEnd());
347 
348 			int width = gx2 - gx1;
349 
350 			if ((cx2 - cx1) <= 5) {
351 				// when the pixel range is narrow, draw a rectangle only 
352 				drawRegion(r, y);
353 			}
354 			else {
355 
356 				boolean drawBase = window.getGenomeRange() <= (image.getWidth() / style.fontWidth);
357 
358 				CIGAR cigar = new CIGAR(r.cigar);
359 				int readStart = r.getStart();
360 				int seqIndex = 0;
361 
362 				// Drawing insertions should be postponed after all of he read bases are painted.
363 				List<PostponedInsertion> postponed = new ArrayList<PostponedInsertion>();
364 				for (int cigarIndex = 0; cigarIndex < cigar.size(); cigarIndex++) {
365 					CIGAR.Element e = cigar.get(cigarIndex);
366 					int readEnd = readStart + e.length;
367 					switch (e.type) {
368 					case Deletions:
369 						// ref : AAAAAA
370 						// read: ---AAA
371 						// cigar: 3D3M
372 						drawPadding(readStart, readEnd, y, style.getReadColor(r));
373 						break;
374 					case Insertions:
375 						// ref : ---AAA
376 						// read: AAAAAA
377 						// cigar: 3I3M
378 						if (r.getSequence() != null)
379 							postponed.add(new PostponedInsertion(readStart, r.getSequence().substring(seqIndex, seqIndex + e.length)));
380 						readEnd = readStart;
381 						seqIndex += e.length;
382 						break;
383 					case Padding:
384 						// ref : AAAAAA
385 						// read: ---AAA
386 						// cigar: 3P3M
387 						readEnd = readStart;
388 						drawPadding(readStart, readStart + 1, y, style.COLOR_PADDING);
389 						break;
390 					case Matches: {
391 
392 						if (drawBase && r.getSequence() != null) {
393 							drawBases(readStart, y, r.getSequence().substring(seqIndex, seqIndex + e.length),
394 									r.getQV() != null ? r.getQV().substring(seqIndex, seqIndex + e.length) : null);
395 						}
396 						else {
397 							drawGeneRect(readStart, readEnd, y, style.getReadColor(r));
398 						}
399 
400 						seqIndex += e.length;
401 					}
402 						break;
403 					case SkippedRegion:
404 						drawPadding(readStart, readEnd, y, style.getReadColor(r));
405 						break;
406 					case SoftClip: {
407 						int softclipStart = cigarIndex == 0 ? readStart - e.length : readStart;
408 						int softclipEnd = cigarIndex == 0 ? readStart : readStart + e.length;
409 						readEnd = softclipEnd;
410 
411 						if (drawBase && r.getSequence() != null) {
412 							drawBases(softclipStart, y, r.getSequence().substring(seqIndex, seqIndex + e.length).toLowerCase(), r.getQV() != null ? r.getQV()
413 									.substring(seqIndex, seqIndex + e.length) : null);
414 						}
415 						else {
416 							drawGeneRect(softclipStart, softclipEnd, y, style.getClippedReadColor(r));
417 						}
418 
419 						seqIndex += e.length;
420 					}
421 						break;
422 					case HardClip:
423 						break;
424 					}
425 					readStart = readEnd;
426 				}
427 
428 				for (PostponedInsertion each : postponed) {
429 					drawGeneRect(each.start, each.start + 1, y, new Color(0x11, 0x11, 0x11));
430 				}
431 			}
432 
433 		}
434 		catch (UTGBClientException e) {
435 			// when parsing CIGAR string fails, simply draw a rectangle
436 			drawRegion(r, y);
437 		}
438 	}
439 
440 	public void drawBases(int startOnGenome, int y, String seq, String qual) {
441 
442 		Font f = new Font("SansSerif", Font.PLAIN, 1);
443 		f = f.deriveFont(style.fontWidth);
444 		g.setFont(f);
445 
446 		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
447 		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
448 
449 		for (int i = 0; i < seq.length(); i++) {
450 			int baseIndex = 8;
451 			char base = seq.charAt(i);
452 			Color c = style.getBaseColor(base);
453 
454 			drawRegion(startOnGenome + i, startOnGenome + i + 1, y, c, false);
455 			drawBase(base, startOnGenome + i, y, Color.WHITE);
456 		}
457 
458 	}
459 
460 	public int getPixelWidth() {
461 		return image.getWidth();
462 	}
463 
464 	public void drawBase(char base, long startIndexOnGenome, int yOffset, Color color) {
465 		int start = window.getXPosOnWindow(startIndexOnGenome, getPixelWidth());
466 		int end = window.getXPosOnWindow(startIndexOnGenome + 1, getPixelWidth());
467 		int drawStart;
468 
469 		String b = Character.toString(base);
470 		g.setColor(color);
471 		FontMetrics fontMetrics = g.getFontMetrics();
472 		int fontWidth = fontMetrics.stringWidth(b);
473 
474 		drawStart = (int) (start + (end - start) / 2.0f - fontWidth / 2.0f);
475 		if (drawStart < 0)
476 			drawStart = end;
477 
478 		g.drawString(b, drawStart, yOffset);
479 	}
480 
481 }