View Javadoc

1   /*--------------------------------------------------------------------------
2    *  Copyright 2009 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  // IntervalLayout.java
20  // Since: 2010/05/27
21  //
22  // $URL$ 
23  // $Author$
24  //--------------------------------------
25  package org.utgenome.gwt.utgb.client.canvas;
26  
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Comparator;
30  import java.util.HashSet;
31  import java.util.List;
32  
33  import org.utgenome.gwt.utgb.client.bio.Interval;
34  import org.utgenome.gwt.utgb.client.bio.OnGenome;
35  import org.utgenome.gwt.utgb.client.bio.OnGenomeDataVisitorBase;
36  import org.utgenome.gwt.utgb.client.bio.ReadCoverage;
37  import org.utgenome.gwt.utgb.client.bio.SAMReadLight;
38  import org.utgenome.gwt.utgb.client.bio.SAMReadPair;
39  import org.utgenome.gwt.utgb.client.bio.SAMReadPairFragment;
40  import org.utgenome.gwt.utgb.client.track.TrackWindow;
41  
42  /**
43   * On-genome data layout
44   * 
45   * @author leo
46   * 
47   */
48  public class IntervalLayout {
49  
50  	private boolean keepSpaceForLabels = true;
51  	private boolean hasEnoughHeightForLabels = false;
52  	private boolean allowPairedReadsOverlaop = true;
53  	private PrioritySearchTree<OnGenome> globalLayout = new PrioritySearchTree<OnGenome>();
54  	private PrioritySearchTree<LocusLayout> localLayoutInView = new PrioritySearchTree<LocusLayout>();
55  
56  	private TrackWindow w;
57  
58  	public static class LocusLayout {
59  		private OnGenome locus;
60  		private int yOffset;
61  		private int height = 1;
62  
63  		public LocusLayout(OnGenome locus, int yOffset) {
64  			this.locus = locus;
65  			this.yOffset = yOffset;
66  		}
67  
68  		public LocusLayout(OnGenome locus, int yOffset, int height) {
69  			this.locus = locus;
70  			this.yOffset = yOffset;
71  			this.height = height;
72  		}
73  
74  		public int getHeight() {
75  			return height;
76  		}
77  
78  		public OnGenome getLocus() {
79  			return locus;
80  		}
81  
82  		public int getYOffset() {
83  			return yOffset;
84  		}
85  
86  		public int scaledHeight(int scale) {
87  			return scaledHeight(yOffset, scale);
88  		}
89  
90  		public static int scaledHeight(int y, int scale) {
91  			return y * scale;
92  		}
93  
94  	}
95  
96  	public IntervalLayout() {
97  	}
98  
99  	public void setTrackWindow(TrackWindow window) {
100 		this.w = window;
101 	}
102 
103 	public static int estimiateLabelWidth(OnGenome l, int geneHeight) {
104 		String name = l.getName();
105 		int labelWidth = name != null ? (int) (name.length() * geneHeight * 0.9) : 0;
106 		if (labelWidth > 150)
107 			labelWidth = 150;
108 		return labelWidth;
109 	}
110 
111 	public List<OnGenome> activeReads() {
112 		final ArrayList<OnGenome> activeData = new ArrayList<OnGenome>();
113 		globalLayout.depthFirstSearch(new PrioritySearchTree.Visitor<OnGenome>() {
114 			public void visit(OnGenome l) {
115 				if (w.overlapWith(l))
116 					activeData.add(l);
117 			}
118 		});
119 		return activeData;
120 	}
121 
122 	/**
123 	 * Retrieves the interval (x1, x2) and the height of OnGenome objects
124 	 * 
125 	 * @author leo
126 	 * 
127 	 */
128 	public static class IntervalRetriever extends OnGenomeDataVisitorBase {
129 
130 		public boolean allowPEOverlap = false;
131 		public int start = -1;
132 		public int end = -1;
133 		public int height = 1;
134 		public boolean isDefined = false;
135 
136 		public void clear() {
137 			isDefined = false;
138 			height = 1;
139 		}
140 
141 		@Override
142 		public void visitInterval(Interval interval) {
143 			start = interval.getStart();
144 			end = interval.getEnd();
145 			isDefined = true;
146 		}
147 
148 		@Override
149 		public void visitSAMReadLight(SAMReadLight r) {
150 
151 			start = r.unclippedStart;
152 			end = r.unclippedEnd;
153 
154 			isDefined = true;
155 		}
156 
157 		@Override
158 		public void visitReadCoverage(ReadCoverage readCoverage) {
159 			this.height = Math.abs(readCoverage.maxHeight - readCoverage.minHeight);
160 			visitInterval(readCoverage);
161 		}
162 
163 		@Override
164 		public void visitSAMReadPairFragment(SAMReadPairFragment fragment) {
165 			start = fragment.getStart();
166 			end = fragment.getEnd();
167 			isDefined = true;
168 		}
169 
170 		@Override
171 		public void visitSAMReadPair(SAMReadPair pair) {
172 			start = pair.getStart();
173 			end = pair.getEnd();
174 			isDefined = true;
175 
176 			if (!allowPEOverlap && pair.getFirst().unclippedSequenceHasOverlapWith(pair.getSecond())) {
177 				height = 2;
178 			}
179 		}
180 
181 	}
182 
183 	private static class DepthMeasure implements PrioritySearchTree.ResultHandler<LocusLayout> {
184 		int maxDepth = 0;
185 
186 		public void handle(LocusLayout l) {
187 			if (maxDepth < l.getYOffset()) {
188 				maxDepth = l.getYOffset();
189 			}
190 		}
191 
192 		public boolean toContinue() {
193 			return true;
194 		}
195 	}
196 
197 	public int maxDepth(TrackWindow view) {
198 		int x1 = pixelPositionOnWindow(view.getStartOnGenome());
199 		int x2 = pixelPositionOnWindow(view.getEndOnGenome());
200 		int maxDepth = 0;
201 
202 		DepthMeasure dm = new DepthMeasure();
203 		localLayoutInView.rangeQuery(x1, Integer.MAX_VALUE, x2, dm);
204 		return dm.maxDepth;
205 	}
206 
207 	/**
208 	 * Creates an X-Y layout of the given intervals
209 	 * 
210 	 * @param <T>
211 	 * @param intervalList
212 	 * @param geneHeight
213 	 *            * @return
214 	 */
215 	public <T extends OnGenome> int reset(List<T> intervalList, int geneHeight) {
216 
217 		globalLayout.clear();
218 		IntervalRetriever ir = new IntervalRetriever();
219 		for (OnGenome l : intervalList) {
220 			ir.clear();
221 			l.accept(ir);
222 			if (!ir.isDefined)
223 				continue;
224 			globalLayout.insert(l, ir.end, ir.start);
225 		}
226 		return createLocalLayout(geneHeight);
227 	}
228 
229 	private class LayoutGenerator {
230 
231 		private final int geneHeight;
232 
233 		IntervalRetriever ir = new IntervalRetriever();
234 
235 		private boolean toContinue = true;
236 		boolean showLabelsFlag = keepSpaceForLabels;
237 		boolean needRelayout = false;
238 		int maxYOffset = 0;
239 
240 		public LayoutGenerator(int geneHeight) {
241 			this.geneHeight = geneHeight;
242 			ir.allowPEOverlap = allowPairedReadsOverlaop;
243 			reset();
244 		}
245 
246 		/**
247 		 * reset except the show lables flag
248 		 */
249 		public void reset() {
250 			toContinue = true;
251 			needRelayout = false;
252 			maxYOffset = 0;
253 		}
254 
255 		public void handle(OnGenome l) {
256 			ir.clear();
257 			l.accept(ir);
258 			if (!ir.isDefined)
259 				return;
260 
261 			int x1 = pixelPositionOnWindow(ir.start);
262 			int x2 = pixelPositionOnWindow(ir.end);
263 
264 			if (showLabelsFlag) {
265 				int labelWidth = estimiateLabelWidth(l, geneHeight);
266 				if (x1 - labelWidth > 0)
267 					x1 -= labelWidth;
268 				else
269 					x2 += labelWidth;
270 			}
271 
272 			List<LocusLayout> activeLocus = localLayoutInView.rangeQuery(x1, Integer.MAX_VALUE, x2);
273 
274 			HashSet<Integer> filledY = new HashSet<Integer>();
275 			// overlap test
276 			for (LocusLayout al : activeLocus) {
277 				for (int i = 0; i < al.getHeight(); i++)
278 					filledY.add(al.yOffset + i);
279 			}
280 
281 			int blankY = 0;
282 			for (; filledY.contains(blankY); blankY++) {
283 			}
284 
285 			localLayoutInView.insert(new LocusLayout(l, blankY, ir.height), x2, x1);
286 			if (blankY > maxYOffset) {
287 				maxYOffset = blankY;
288 				if (showLabelsFlag && maxYOffset > 30) {
289 					showLabelsFlag = false;
290 					// reset the current layout, then create another layout without the read labels 
291 					needRelayout = true;
292 					toContinue = false;
293 				}
294 			}
295 
296 		}
297 
298 		public boolean toContinue() {
299 			return toContinue;
300 		}
301 
302 	}
303 
304 	/**
305 	 * Creates an X-Y layout of the given intervals, then return the max depth of the intervals
306 	 * 
307 	 * @param <T>
308 	 * @param intervalList
309 	 * @param geneHeight
310 	 *            * @return
311 	 */
312 	public <T extends OnGenome> int createLocalLayout(final int geneHeight) {
313 
314 		//int maxYOffset = 0;
315 
316 		List<OnGenome> readsInRange = globalLayout.rangeQuery(w.getStartOnGenome(), Integer.MAX_VALUE, w.getEndOnGenome());
317 
318 		Collections.sort(readsInRange, new Comparator<OnGenome>() {
319 			public int compare(OnGenome o1, OnGenome o2) {
320 				int diff = o1.getStart() - o2.getStart();
321 				if (diff == 0)
322 					return o1.length() - o2.length();
323 				else
324 					return diff;
325 			}
326 		});
327 
328 		LayoutGenerator layoutGenerator = new LayoutGenerator(geneHeight);
329 		boolean toContinue = false;
330 		do {
331 			localLayoutInView.clear();
332 			layoutGenerator.reset();
333 
334 			for (OnGenome each : readsInRange) {
335 				layoutGenerator.handle(each);
336 				toContinue = layoutGenerator.needRelayout;
337 				if (!layoutGenerator.toContinue())
338 					break;
339 			}
340 		}
341 		while (toContinue);
342 
343 		int maxYOffset = layoutGenerator.maxYOffset;
344 		if (maxYOffset <= 0)
345 			maxYOffset = 1;
346 
347 		hasEnoughHeightForLabels = layoutGenerator.showLabelsFlag;
348 		return maxYOffset;
349 	}
350 
351 	public void setKeepSpaceForLabels(boolean keep) {
352 		this.keepSpaceForLabels = keep;
353 	}
354 
355 	public boolean keepSpaceForLabels() {
356 		return this.keepSpaceForLabels;
357 	}
358 
359 	public boolean hasEnoughHeightForLabels() {
360 		return hasEnoughHeightForLabels;
361 	}
362 
363 	public void depthFirstSearch(PrioritySearchTree.Visitor<LocusLayout> visitor) {
364 		localLayoutInView.depthFirstSearch(visitor);
365 	}
366 
367 	/**
368 	 * Resolving actual read contained in SAMReadPair block (x, y)
369 	 * 
370 	 * |--------------(SAMReadPair)-----------------|
371 	 * 
372 	 * ||--(first pair:SAMRead)----| | | |-----(second pair:SAMRead)-----||
373 	 * 
374 	 * @author leo
375 	 * 
376 	 */
377 	private static class PairedEndTargetResolver extends OnGenomeDataVisitorBase {
378 		private OnGenome target;
379 		private final int start;
380 		private final int yOffset;
381 
382 		public PairedEndTargetResolver(OnGenome g, int start, int yOffset) {
383 			this.target = g;
384 			this.start = start;
385 			this.yOffset = yOffset;
386 		}
387 
388 		@Override
389 		public void visitSAMReadPair(SAMReadPair pair) {
390 			boolean overlapWithFirst = pair.getFirst().unclippedSequenceContains(start);
391 			boolean overlapWithSecond = pair.getSecond().unclippedSequenceContains(start);
392 
393 			if (overlapWithFirst) {
394 				if (overlapWithSecond) {
395 					this.target = yOffset == 0 ? pair.getFirst() : pair.getSecond();
396 				}
397 				else
398 					this.target = pair.getFirst();
399 			}
400 			else if (overlapWithSecond)
401 				this.target = pair.getSecond();
402 
403 		}
404 
405 		public static OnGenome resolve(OnGenome g, int start, int yOffset) {
406 			PairedEndTargetResolver pe = new PairedEndTargetResolver(g, start, yOffset);
407 			g.accept(pe);
408 			return pe.target;
409 		}
410 	}
411 
412 	/**
413 	 * compute the overlapped intervals for the mouse over event
414 	 * 
415 	 * @param event
416 	 * @param xBorder
417 	 * @return
418 	 */
419 	public OnGenome overlappedInterval(int x, int y, int xBorder, int geneHeight) {
420 
421 		for (LocusLayout gl : localLayoutInView.rangeQuery(x, Integer.MAX_VALUE, x)) {
422 			OnGenome g = gl.getLocus();
423 			int y1 = gl.getYOffset() * geneHeight;
424 			int y2 = y1 + geneHeight * gl.getHeight();
425 
426 			if (y1 <= y && y <= y2) {
427 				int x1 = pixelPositionOnWindow(g.getStart()) - xBorder;
428 				int x2 = pixelPositionOnWindow(g.getStart() + g.length()) + xBorder;
429 
430 				if (hasEnoughHeightForLabels) {
431 					int labelWidth = estimiateLabelWidth(g, geneHeight);
432 					if (x1 - labelWidth > 0)
433 						x1 -= labelWidth;
434 					else
435 						x2 += labelWidth;
436 				}
437 
438 				if (x1 <= x && x <= x2) {
439 					int start = w.convertToGenomePosition(x);
440 					g = PairedEndTargetResolver.resolve(g, start, (y - y1) / geneHeight);
441 					return g;
442 				}
443 			}
444 		}
445 		return null;
446 
447 	}
448 
449 	public int pixelPositionOnWindow(int indexOnGenome) {
450 		return w.convertToPixelX(indexOnGenome);
451 	}
452 
453 	public void clear() {
454 		localLayoutInView.clear();
455 		globalLayout.clear();
456 	}
457 
458 	public void setAllowOverlapPairedReads(boolean allow) {
459 		this.allowPairedReadsOverlaop = allow;
460 	}
461 
462 }