1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
60
61
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
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
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
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
370
371
372 drawPadding(readStart, readEnd, y, style.getReadColor(r));
373 break;
374 case Insertions:
375
376
377
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
385
386
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
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 }