Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
SeqCanvas | 58 | 679 | 211 |
1 | /* | |
2 | * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) | |
3 | * Copyright (C) $$Year-Rel$$ The Jalview Authors | |
4 | * | |
5 | * This file is part of Jalview. | |
6 | * | |
7 | * Jalview is free software: you can redistribute it and/or | |
8 | * modify it under the terms of the GNU General Public License | |
9 | * as published by the Free Software Foundation, either version 3 | |
10 | * of the License, or (at your option) any later version. | |
11 | * | |
12 | * Jalview is distributed in the hope that it will be useful, but | |
13 | * WITHOUT ANY WARRANTY; without even the implied warranty | |
14 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR | |
15 | * PURPOSE. See the GNU General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with Jalview. If not, see <http://www.gnu.org/licenses/>. | |
19 | * The Jalview Authors are detailed in the 'AUTHORS' file. | |
20 | */ | |
21 | package jalview.gui; | |
22 | ||
23 | import java.awt.BasicStroke; | |
24 | import java.awt.BorderLayout; | |
25 | import java.awt.Color; | |
26 | import java.awt.FontMetrics; | |
27 | import java.awt.Graphics; | |
28 | import java.awt.Graphics2D; | |
29 | import java.awt.Rectangle; | |
30 | import java.awt.RenderingHints; | |
31 | import java.awt.image.BufferedImage; | |
32 | import java.beans.PropertyChangeEvent; | |
33 | import java.util.Iterator; | |
34 | import java.util.List; | |
35 | ||
36 | import javax.swing.JPanel; | |
37 | ||
38 | import jalview.api.SequenceRenderer; | |
39 | import jalview.datamodel.AlignmentI; | |
40 | import jalview.datamodel.HiddenColumns; | |
41 | import jalview.datamodel.SearchResultsI; | |
42 | import jalview.datamodel.SequenceGroup; | |
43 | import jalview.datamodel.SequenceI; | |
44 | import jalview.datamodel.VisibleContigsIterator; | |
45 | import jalview.renderer.ScaleRenderer; | |
46 | import jalview.renderer.ScaleRenderer.ScaleMark; | |
47 | import jalview.util.Comparison; | |
48 | import jalview.viewmodel.ViewportListenerI; | |
49 | import jalview.viewmodel.ViewportRanges; | |
50 | ||
51 | /** | |
52 | * The Swing component on which the alignment sequences, and annotations (if | |
53 | * shown), are drawn. This includes scales above, left and right (if shown) in | |
54 | * Wrapped mode, but not the scale above in Unwrapped mode. | |
55 | * | |
56 | */ | |
57 | @SuppressWarnings("serial") | |
58 | public class SeqCanvas extends JPanel implements ViewportListenerI | |
59 | { | |
60 | /** | |
61 | * vertical gap in pixels between sequences and annotations when in wrapped | |
62 | * mode | |
63 | */ | |
64 | static final int SEQS_ANNOTATION_GAP = 3; | |
65 | ||
66 | private static final String ZEROS = "0000000000"; | |
67 | ||
68 | final FeatureRenderer fr; | |
69 | ||
70 | BufferedImage img; | |
71 | ||
72 | AlignViewport av; | |
73 | ||
74 | int cursorX = 0; | |
75 | ||
76 | int cursorY = 0; | |
77 | ||
78 | private final SequenceRenderer seqRdr; | |
79 | ||
80 | boolean fastPaint = false; | |
81 | ||
82 | private boolean fastpainting = false; | |
83 | ||
84 | private AnnotationPanel annotations; | |
85 | ||
86 | /* | |
87 | * measurements for drawing a wrapped alignment | |
88 | */ | |
89 | private int labelWidthEast; // label right width in pixels if shown | |
90 | ||
91 | private int labelWidthWest; // label left width in pixels if shown | |
92 | ||
93 | int wrappedSpaceAboveAlignment; // gap between widths | |
94 | ||
95 | int wrappedRepeatHeightPx; // height in pixels of wrapped width | |
96 | ||
97 | private int wrappedVisibleWidths; // number of wrapped widths displayed | |
98 | ||
99 | // Don't do this! Graphics handles are supposed to be transient | |
100 | // private Graphics2D gg; | |
101 | ||
102 | /** | |
103 | * Creates a new SeqCanvas object. | |
104 | * | |
105 | * @param ap | |
106 | */ | |
107 | 478 | public SeqCanvas(AlignmentPanel ap) |
108 | { | |
109 | 478 | this.av = ap.av; |
110 | 478 | fr = new FeatureRenderer(ap); |
111 | 478 | seqRdr = new jalview.gui.SequenceRenderer(av); |
112 | 478 | setLayout(new BorderLayout()); |
113 | 478 | PaintRefresher.Register(this, av.getSequenceSetId()); |
114 | 478 | setBackground(Color.white); |
115 | ||
116 | 478 | av.getRanges().addPropertyChangeListener(this); |
117 | } | |
118 | ||
119 | 6 | public SequenceRenderer getSequenceRenderer() |
120 | { | |
121 | 6 | return seqRdr; |
122 | } | |
123 | ||
124 | 689 | public FeatureRenderer getFeatureRenderer() |
125 | { | |
126 | 689 | return fr; |
127 | } | |
128 | ||
129 | /** | |
130 | * Draws the scale above a region of a wrapped alignment, consisting of a | |
131 | * column number every major interval (10 columns). | |
132 | * | |
133 | * @param g | |
134 | * the graphics context to draw on, positioned at the start (bottom | |
135 | * left) of the line on which to draw any scale marks | |
136 | * @param startx | |
137 | * start alignment column (0..) | |
138 | * @param endx | |
139 | * end alignment column (0..) | |
140 | * @param ypos | |
141 | * y offset to draw at | |
142 | */ | |
143 | 0 | private void drawNorthScale(Graphics g, int startx, int endx, int ypos) |
144 | { | |
145 | 0 | int charHeight = av.getCharHeight(); |
146 | 0 | int charWidth = av.getCharWidth(); |
147 | ||
148 | /* | |
149 | * white fill the scale space (for the fastPaint case) | |
150 | */ | |
151 | 0 | g.setColor(Color.white); |
152 | 0 | g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(), |
153 | charHeight * 3 / 2 + 2); | |
154 | 0 | g.setColor(Color.black); |
155 | ||
156 | 0 | List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx, |
157 | endx); | |
158 | 0 | for (ScaleMark mark : marks) |
159 | { | |
160 | 0 | int mpos = mark.column; // (i - startx - 1) |
161 | 0 | if (mpos < 0) |
162 | { | |
163 | 0 | continue; |
164 | } | |
165 | 0 | String mstring = mark.text; |
166 | ||
167 | 0 | if (mark.major) |
168 | { | |
169 | 0 | if (mstring != null) |
170 | { | |
171 | 0 | g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2)); |
172 | } | |
173 | ||
174 | /* | |
175 | * draw a tick mark below the column number, centred on the column; | |
176 | * height of tick mark is 4 pixels less than half a character | |
177 | */ | |
178 | 0 | int xpos = (mpos * charWidth) + (charWidth / 2); |
179 | 0 | g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2); |
180 | } | |
181 | } | |
182 | } | |
183 | ||
184 | /** | |
185 | * Draw the scale to the left or right of a wrapped alignment | |
186 | * | |
187 | * @param g | |
188 | * graphics context, positioned at the start of the scale to be drawn | |
189 | * @param startx | |
190 | * first column of wrapped width (0.. excluding any hidden columns) | |
191 | * @param endx | |
192 | * last column of wrapped width (0.. excluding any hidden columns) | |
193 | * @param ypos | |
194 | * vertical offset at which to begin the scale | |
195 | * @param left | |
196 | * if true, scale is left of residues, if false, scale is right | |
197 | */ | |
198 | 508 | void drawVerticalScale(Graphics g, final int startx, final int endx, |
199 | final int ypos, final boolean left) | |
200 | { | |
201 | 508 | int charHeight = av.getCharHeight(); |
202 | 508 | int charWidth = av.getCharWidth(); |
203 | ||
204 | 508 | int yPos = ypos + charHeight; |
205 | 508 | int startX = startx; |
206 | 508 | int endX = endx; |
207 | ||
208 | 508 | if (av.hasHiddenColumns()) |
209 | { | |
210 | 500 | HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns(); |
211 | 500 | startX = hiddenColumns.visibleToAbsoluteColumn(startx); |
212 | 500 | endX = hiddenColumns.visibleToAbsoluteColumn(endx); |
213 | } | |
214 | 508 | FontMetrics fm = getFontMetrics(av.getFont()); |
215 | ||
216 | 1636 | for (int i = 0; i < av.getAlignment().getHeight(); i++) |
217 | { | |
218 | 1128 | SequenceI seq = av.getAlignment().getSequenceAt(i); |
219 | ||
220 | /* | |
221 | * find sequence position of first non-gapped position - | |
222 | * to the right if scale left, to the left if scale right | |
223 | */ | |
224 | 1128 | int index = left ? startX : endX; |
225 | 1128 | int value = -1; |
226 | 1368 | while (index >= startX && index <= endX) |
227 | { | |
228 | 1368 | if (!Comparison.isGap(seq.getCharAt(index))) |
229 | { | |
230 | 1128 | value = seq.findPosition(index); |
231 | 1128 | break; |
232 | } | |
233 | 240 | if (left) |
234 | { | |
235 | 0 | index++; |
236 | } | |
237 | else | |
238 | { | |
239 | 240 | index--; |
240 | } | |
241 | } | |
242 | ||
243 | /* | |
244 | * white fill the space for the scale | |
245 | */ | |
246 | 1128 | g.setColor(Color.white); |
247 | 1128 | int y = (yPos + (i * charHeight)) - (charHeight / 5); |
248 | // fillRect origin is top left of rectangle | |
249 | 1128 | g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast, |
250 | charHeight + 1); | |
251 | ||
252 | 1128 | if (value != -1) |
253 | { | |
254 | /* | |
255 | * draw scale value, right justified within its width less half a | |
256 | * character width padding on the right | |
257 | */ | |
258 | 1128 | int labelSpace = left ? labelWidthWest : labelWidthEast; |
259 | 1128 | labelSpace -= charWidth / 2; // leave space to the right |
260 | 1128 | String valueAsString = String.valueOf(value); |
261 | 1128 | int labelLength = fm.stringWidth(valueAsString); |
262 | 1128 | int xOffset = labelSpace - labelLength; |
263 | 1128 | g.setColor(Color.black); |
264 | 1128 | g.drawString(valueAsString, xOffset, y); |
265 | } | |
266 | } | |
267 | ||
268 | } | |
269 | ||
270 | /** | |
271 | * Does a fast paint of an alignment in response to a scroll. Most of the | |
272 | * visible region is simply copied and shifted, and then any newly visible | |
273 | * columns or rows are drawn. The scroll may be horizontal or vertical, but | |
274 | * not both at once. Scrolling may be the result of | |
275 | * <ul> | |
276 | * <li>dragging a scroll bar</li> | |
277 | * <li>clicking in the scroll bar</li> | |
278 | * <li>scrolling by trackpad, middle mouse button, or other device</li> | |
279 | * <li>by moving the box in the Overview window</li> | |
280 | * <li>programmatically to make a highlighted position visible</li> | |
281 | * <li>pasting a block of sequences</li> | |
282 | * </ul> | |
283 | * | |
284 | * @param horizontal | |
285 | * columns to shift right (positive) or left (negative) | |
286 | * @param vertical | |
287 | * rows to shift down (positive) or up (negative) | |
288 | */ | |
289 | 77 | public void fastPaint(int horizontal, int vertical) |
290 | { | |
291 | ||
292 | // effectively: | |
293 | // if (horizontal != 0 && vertical != 0) | |
294 | // throw new InvalidArgumentException(); | |
295 | 77 | if (fastpainting || img == null) |
296 | { | |
297 | 54 | return; |
298 | } | |
299 | 23 | fastpainting = true; |
300 | 23 | fastPaint = true; |
301 | 23 | try |
302 | { | |
303 | 23 | int charHeight = av.getCharHeight(); |
304 | 23 | int charWidth = av.getCharWidth(); |
305 | ||
306 | 23 | ViewportRanges ranges = av.getRanges(); |
307 | 23 | int startRes = ranges.getStartRes(); |
308 | 23 | int endRes = ranges.getEndRes(); |
309 | 23 | int startSeq = ranges.getStartSeq(); |
310 | 23 | int endSeq = ranges.getEndSeq(); |
311 | 23 | int transX = 0; |
312 | 23 | int transY = 0; |
313 | ||
314 | 23 | if (horizontal > 0) // scrollbar pulled right, image to the left |
315 | { | |
316 | 1 | transX = (endRes - startRes - horizontal) * charWidth; |
317 | 1 | startRes = endRes - horizontal; |
318 | } | |
319 | 22 | else if (horizontal < 0) |
320 | { | |
321 | 0 | endRes = startRes - horizontal; |
322 | } | |
323 | ||
324 | 23 | if (vertical > 0) // scroll down |
325 | { | |
326 | 11 | startSeq = endSeq - vertical + 1; |
327 | ||
328 | 11 | if (startSeq < ranges.getStartSeq()) |
329 | { // ie scrolling too fast, more than a page at a time | |
330 | 1 | startSeq = ranges.getStartSeq(); |
331 | } | |
332 | else | |
333 | { | |
334 | 10 | transY = img.getHeight() - (vertical * charHeight); |
335 | } | |
336 | } | |
337 | 12 | else if (vertical < 0) // scroll up |
338 | { | |
339 | 11 | endSeq = startSeq - vertical - 1; |
340 | ||
341 | 11 | if (endSeq > ranges.getEndSeq()) |
342 | { | |
343 | 0 | endSeq = ranges.getEndSeq(); |
344 | } | |
345 | } | |
346 | ||
347 | // jalview.bin.Console.errPrintln(">>> FastPaint to " + transX + " " + | |
348 | // transY + " " | |
349 | // + horizontal + " " + vertical + " " + startRes + " " + endRes | |
350 | // + " " + startSeq + " " + endSeq); | |
351 | ||
352 | 23 | Graphics gg = img.getGraphics(); |
353 | 23 | gg.copyArea(horizontal * charWidth, vertical * charHeight, |
354 | img.getWidth(), img.getHeight(), -horizontal * charWidth, | |
355 | -vertical * charHeight); | |
356 | ||
357 | /** @j2sNative xxi = this.img */ | |
358 | ||
359 | 23 | gg.translate(transX, transY); |
360 | 23 | drawPanel(seqRdr, gg, startRes, endRes, startSeq, endSeq, 0); |
361 | 23 | gg.translate(-transX, -transY); |
362 | 23 | gg.dispose(); |
363 | ||
364 | // Call repaint on alignment panel so that repaints from other alignment | |
365 | // panel components can be aggregated. Otherwise performance of the | |
366 | // overview window and others may be adversely affected. | |
367 | // jalview.bin.Console.outPrintln("SeqCanvas fastPaint() repaint() | |
368 | // request..."); | |
369 | 23 | av.getAlignPanel().repaint(); |
370 | } finally | |
371 | { | |
372 | 23 | fastpainting = false; |
373 | } | |
374 | } | |
375 | ||
376 | 2854 | @Override |
377 | public void paintComponent(Graphics g) | |
378 | { | |
379 | ||
380 | 2854 | int charHeight = av.getCharHeight(); |
381 | 2854 | int charWidth = av.getCharWidth(); |
382 | ||
383 | 2854 | int width = getWidth(); |
384 | 2854 | int height = getHeight(); |
385 | ||
386 | 2854 | width -= (width % charWidth); |
387 | 2854 | height -= (height % charHeight); |
388 | ||
389 | // BH 2019 can't possibly fastPaint if either width or height is 0 | |
390 | ||
391 | 2854 | if (width == 0 || height == 0) |
392 | { | |
393 | 0 | return; |
394 | } | |
395 | ||
396 | 2854 | ViewportRanges ranges = av.getRanges(); |
397 | 2854 | int startRes = ranges.getStartRes(); |
398 | 2854 | int startSeq = ranges.getStartSeq(); |
399 | 2854 | int endRes = ranges.getEndRes(); |
400 | 2854 | int endSeq = ranges.getEndSeq(); |
401 | ||
402 | // [JAL-3226] problem that JavaScript (or Java) may consolidate multiple | |
403 | // repaint() requests in unpredictable ways. In this case, the issue was | |
404 | // that in response to a CTRL-C/CTRL-V paste request, in Java a fast | |
405 | // repaint request preceded two full requests, thus resulting | |
406 | // in a full request for paint. In constrast, in JavaScript, the three | |
407 | // requests were bundled together into one, so the fastPaint flag was | |
408 | // still present for the second and third request. | |
409 | // | |
410 | // This resulted in incomplete painting. | |
411 | // | |
412 | // The solution was to set seqCanvas.fastPaint and idCanvas.fastPaint false | |
413 | // in PaintRefresher when the target to be painted is one of those two | |
414 | // components. | |
415 | // | |
416 | // BH 2019.04.22 | |
417 | // | |
418 | // An initial idea; can be removed once we determine this issue is closed: | |
419 | // if (av.isFastPaintDisabled()) | |
420 | // { | |
421 | // fastPaint = false; | |
422 | // } | |
423 | ||
424 | 2854 | Rectangle vis, clip; |
425 | ? | if (img != null |
426 | && (fastPaint | |
427 | || (vis = getVisibleRect()).width != (clip = g | |
428 | .getClipBounds()).width | |
429 | || vis.height != clip.height)) | |
430 | { | |
431 | 257 | g.drawImage(img, 0, 0, this); |
432 | 257 | drawSelectionGroup((Graphics2D) g, startRes, endRes, startSeq, |
433 | endSeq); | |
434 | 257 | fastPaint = false; |
435 | } | |
436 | else | |
437 | { | |
438 | // img is a cached version of the last view we drew. | |
439 | // If we have no img or the size has changed, make a new one. | |
440 | // | |
441 | 2597 | if (img == null || width != img.getWidth() |
442 | || height != img.getHeight()) | |
443 | { | |
444 | 418 | img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
445 | } | |
446 | ||
447 | 2597 | Graphics2D gg = (Graphics2D) img.getGraphics(); |
448 | 2597 | gg.setFont(av.getFont()); |
449 | ||
450 | 2597 | if (av.antiAlias) |
451 | { | |
452 | 702 | gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
453 | RenderingHints.VALUE_ANTIALIAS_ON); | |
454 | } | |
455 | ||
456 | 2597 | gg.setColor(Color.white); |
457 | 2597 | gg.fillRect(0, 0, img.getWidth(), img.getHeight()); |
458 | ||
459 | 2597 | if (av.getWrapAlignment()) |
460 | { | |
461 | 254 | drawWrappedPanel(seqRdr, gg, getWidth(), getHeight(), |
462 | ranges.getStartRes()); | |
463 | } | |
464 | else | |
465 | { | |
466 | 2343 | drawPanel(seqRdr, gg, startRes, endRes, startSeq, endSeq, 0); |
467 | } | |
468 | ||
469 | 2597 | drawSelectionGroup(gg, startRes, endRes, startSeq, endSeq); |
470 | ||
471 | 2597 | g.drawImage(img, 0, 0, this); |
472 | 2597 | gg.dispose(); |
473 | } | |
474 | ||
475 | 2854 | if (av.cursorMode) |
476 | { | |
477 | 0 | drawCursor(g, startRes, endRes, startSeq, endSeq); |
478 | } | |
479 | } | |
480 | ||
481 | /** | |
482 | * Draw an alignment panel for printing | |
483 | * | |
484 | * @param g1 | |
485 | * Graphics object to draw with | |
486 | * @param startRes | |
487 | * start residue of print area | |
488 | * @param endRes | |
489 | * end residue of print area | |
490 | * @param startSeq | |
491 | * start sequence of print area | |
492 | * @param endSeq | |
493 | * end sequence of print area | |
494 | */ | |
495 | 38 | public void drawPanelForPrinting(Graphics g1, int startRes, int endRes, |
496 | int startSeq, int endSeq) | |
497 | { | |
498 | 38 | SequenceRenderer localSeqR = new jalview.gui.SequenceRenderer(av); |
499 | 38 | drawPanel(localSeqR, g1, startRes, endRes, startSeq, endSeq, 0); |
500 | ||
501 | 38 | drawSelectionGroup((Graphics2D) g1, startRes, endRes, startSeq, endSeq); |
502 | } | |
503 | ||
504 | /** | |
505 | * Draw a wrapped alignment panel for printing | |
506 | * | |
507 | * @param g | |
508 | * Graphics object to draw with | |
509 | * @param canvasWidth | |
510 | * width of drawing area | |
511 | * @param canvasHeight | |
512 | * height of drawing area | |
513 | * @param startRes | |
514 | * start residue of print area | |
515 | */ | |
516 | 0 | public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth, |
517 | int canvasHeight, int startRes) | |
518 | { | |
519 | 0 | SequenceRenderer localSeqR = new jalview.gui.SequenceRenderer(av); |
520 | 0 | drawWrappedPanel(localSeqR, g, canvasWidth, canvasHeight, startRes); |
521 | ||
522 | 0 | SequenceGroup group = av.getSelectionGroup(); |
523 | 0 | if (group != null) |
524 | { | |
525 | 0 | drawWrappedSelection((Graphics2D) g, group, canvasWidth, canvasHeight, |
526 | startRes); | |
527 | } | |
528 | } | |
529 | ||
530 | /** | |
531 | * Returns the visible width of the canvas in residues, after allowing for | |
532 | * East or West scales (if shown) | |
533 | * | |
534 | * @param canvasWidth | |
535 | * the width in pixels (possibly including scales) | |
536 | * | |
537 | * @return | |
538 | */ | |
539 | 677 | public int getWrappedCanvasWidth(int canvasWidth) |
540 | { | |
541 | 677 | int charWidth = av.getCharWidth(); |
542 | ||
543 | 677 | FontMetrics fm = getFontMetrics(av.getFont()); |
544 | ||
545 | 677 | int labelWidth = 0; |
546 | ||
547 | 677 | if (av.getScaleRightWrapped() || av.getScaleLeftWrapped()) |
548 | { | |
549 | 566 | labelWidth = getLabelWidth(fm); |
550 | } | |
551 | ||
552 | 677 | labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0; |
553 | ||
554 | 677 | labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0; |
555 | ||
556 | 677 | return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth; |
557 | } | |
558 | ||
559 | 257 | public int getMinimumWrappedCanvasWidth() |
560 | { | |
561 | 257 | int charWidth = av.getCharWidth(); |
562 | 257 | FontMetrics fm = getFontMetrics(av.getFont()); |
563 | 257 | int labelWidth = 0; |
564 | 257 | if (av.getScaleRightWrapped() || av.getScaleLeftWrapped()) |
565 | { | |
566 | 257 | labelWidth = getLabelWidth(fm); |
567 | } | |
568 | 257 | labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0; |
569 | 257 | labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0; |
570 | 257 | return labelWidthEast + labelWidthWest + charWidth; |
571 | } | |
572 | ||
573 | /** | |
574 | * Returns a pixel width sufficient to show the largest sequence coordinate | |
575 | * (end position) in the alignment, calculated as the FontMetrics width of | |
576 | * zeroes "0000000" limited to the number of decimal digits to be shown (3 for | |
577 | * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for | |
578 | * half a character width space on either side. | |
579 | * | |
580 | * @param fm | |
581 | * @return | |
582 | */ | |
583 | 823 | protected int getLabelWidth(FontMetrics fm) |
584 | { | |
585 | /* | |
586 | * find the biggest sequence end position we need to show | |
587 | * (note this is not necessarily the sequence length) | |
588 | */ | |
589 | 823 | int maxWidth = 0; |
590 | 823 | AlignmentI alignment = av.getAlignment(); |
591 | 3023 | for (int i = 0; i < alignment.getHeight(); i++) |
592 | { | |
593 | 2200 | maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd()); |
594 | } | |
595 | ||
596 | 823 | int length = 0; |
597 | 3315 | for (int i = maxWidth; i > 0; i /= 10) |
598 | { | |
599 | 2492 | length++; |
600 | } | |
601 | ||
602 | 823 | return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth(); |
603 | } | |
604 | ||
605 | /** | |
606 | * Draws as many widths of a wrapped alignment as can fit in the visible | |
607 | * window | |
608 | * | |
609 | * @param g | |
610 | * @param canvasWidth | |
611 | * available width in pixels | |
612 | * @param canvasHeight | |
613 | * available height in pixels | |
614 | * @param startColumn | |
615 | * the first column (0...) of the alignment to draw | |
616 | */ | |
617 | 254 | public void drawWrappedPanel(SequenceRenderer seqRdr, Graphics g, |
618 | int canvasWidth, int canvasHeight, final int startColumn) | |
619 | { | |
620 | 254 | int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth, |
621 | canvasHeight); | |
622 | ||
623 | 254 | av.setWrappedWidth(wrappedWidthInResidues); |
624 | ||
625 | 254 | ViewportRanges ranges = av.getRanges(); |
626 | 254 | ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues); |
627 | ||
628 | // we need to call this again to make sure the startColumn + | |
629 | // wrappedWidthInResidues values are used to calculate wrappedVisibleWidths | |
630 | // correctly. | |
631 | 254 | calculateWrappedGeometry(canvasWidth, canvasHeight); |
632 | ||
633 | /* | |
634 | * draw one width at a time (excluding any scales shown), | |
635 | * until we have run out of either alignment or vertical space available | |
636 | */ | |
637 | 254 | int ypos = wrappedSpaceAboveAlignment; |
638 | 254 | int maxWidth = ranges.getVisibleAlignmentWidth(); |
639 | ||
640 | 254 | int start = startColumn; |
641 | 254 | int currentWidth = 0; |
642 | 508 | while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth)) |
643 | { | |
644 | 254 | int endColumn = Math.min(maxWidth, |
645 | start + wrappedWidthInResidues - 1); | |
646 | 254 | drawWrappedWidth(seqRdr, g, ypos, start, endColumn, canvasHeight); |
647 | 254 | ypos += wrappedRepeatHeightPx; |
648 | 254 | start += wrappedWidthInResidues; |
649 | 254 | currentWidth++; |
650 | } | |
651 | ||
652 | 254 | drawWrappedDecorators(g, startColumn); |
653 | } | |
654 | ||
655 | /** | |
656 | * Calculates and saves values needed when rendering a wrapped alignment. | |
657 | * These depend on many factors, including | |
658 | * <ul> | |
659 | * <li>canvas width and height</li> | |
660 | * <li>number of visible sequences, and height of annotations if shown</li> | |
661 | * <li>font and character width</li> | |
662 | * <li>whether scales are shown left, right or above the alignment</li> | |
663 | * </ul> | |
664 | * | |
665 | * @param canvasWidth | |
666 | * @param canvasHeight | |
667 | * @return the number of residue columns in each width | |
668 | */ | |
669 | 588 | protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight) |
670 | { | |
671 | 588 | int charHeight = av.getCharHeight(); |
672 | ||
673 | /* | |
674 | * vertical space in pixels between wrapped widths of alignment | |
675 | * - one character height, or two if scale above is drawn | |
676 | */ | |
677 | 588 | wrappedSpaceAboveAlignment = charHeight |
678 | 588 | * (av.getScaleAboveWrapped() ? 2 : 1); |
679 | ||
680 | /* | |
681 | * compute height in pixels of the wrapped widths | |
682 | * - start with space above plus sequences | |
683 | */ | |
684 | 588 | wrappedRepeatHeightPx = wrappedSpaceAboveAlignment; |
685 | 588 | wrappedRepeatHeightPx += av.getAlignment().getHeight() * charHeight; |
686 | ||
687 | /* | |
688 | * add annotations panel height if shown | |
689 | * also gap between sequences and annotations | |
690 | */ | |
691 | 588 | if (av.isShowAnnotation()) |
692 | { | |
693 | 556 | wrappedRepeatHeightPx += getAnnotationHeight(); |
694 | 556 | wrappedRepeatHeightPx += SEQS_ANNOTATION_GAP; // 3px |
695 | } | |
696 | ||
697 | /* | |
698 | * number of visible widths (the last one may be part height), | |
699 | * ensuring a part height includes at least one sequence | |
700 | */ | |
701 | 588 | ViewportRanges ranges = av.getRanges(); |
702 | 588 | wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx; |
703 | 588 | int remainder = canvasHeight % wrappedRepeatHeightPx; |
704 | 588 | if (remainder >= (wrappedSpaceAboveAlignment + charHeight)) |
705 | { | |
706 | 562 | wrappedVisibleWidths++; |
707 | } | |
708 | ||
709 | /* | |
710 | * compute width in residues; this also sets East and West label widths | |
711 | */ | |
712 | 588 | int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth); |
713 | 588 | av.setWrappedWidth(wrappedWidthInResidues); // update model accordingly |
714 | /* | |
715 | * limit visibleWidths to not exceed width of alignment | |
716 | */ | |
717 | 588 | int xMax = ranges.getVisibleAlignmentWidth(); |
718 | 588 | int startToEnd = xMax - ranges.getStartRes(); |
719 | 588 | int maxWidths = startToEnd / wrappedWidthInResidues; |
720 | 588 | if (startToEnd % wrappedWidthInResidues > 0) |
721 | { | |
722 | 587 | maxWidths++; |
723 | } | |
724 | 588 | wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths); |
725 | ||
726 | 588 | return wrappedWidthInResidues; |
727 | } | |
728 | ||
729 | /** | |
730 | * Draws one width of a wrapped alignment, including sequences and | |
731 | * annnotations, if shown, but not scales or hidden column markers | |
732 | * | |
733 | * @param g | |
734 | * @param ypos | |
735 | * @param startColumn | |
736 | * @param endColumn | |
737 | * @param canvasHeight | |
738 | */ | |
739 | 254 | protected void drawWrappedWidth(SequenceRenderer seqRdr, Graphics g, |
740 | final int ypos, final int startColumn, final int endColumn, | |
741 | final int canvasHeight) | |
742 | { | |
743 | 254 | ViewportRanges ranges = av.getRanges(); |
744 | 254 | int viewportWidth = ranges.getViewportWidth(); |
745 | ||
746 | 254 | int endx = Math.min(startColumn + viewportWidth - 1, endColumn); |
747 | ||
748 | /* | |
749 | * move right before drawing by the width of the scale left (if any) | |
750 | * plus column offset from left margin (usually zero, but may be non-zero | |
751 | * when fast painting is drawing just a few columns) | |
752 | */ | |
753 | 254 | int charWidth = av.getCharWidth(); |
754 | 254 | int xOffset = labelWidthWest |
755 | + ((startColumn - ranges.getStartRes()) % viewportWidth) | |
756 | * charWidth; | |
757 | ||
758 | 254 | g.translate(xOffset, 0); |
759 | ||
760 | /* | |
761 | * white fill the region to be drawn (so incremental fast paint doesn't | |
762 | * scribble over an existing image) | |
763 | */ | |
764 | 254 | g.setColor(Color.white); |
765 | 254 | g.fillRect(0, ypos, (endx - startColumn + 1) * charWidth, |
766 | wrappedRepeatHeightPx); | |
767 | ||
768 | 254 | drawPanel(seqRdr, g, startColumn, endx, 0, |
769 | av.getAlignment().getHeight() - 1, ypos); | |
770 | ||
771 | 254 | int cHeight = av.getAlignment().getHeight() * av.getCharHeight(); |
772 | ||
773 | 254 | if (av.isShowAnnotation()) |
774 | { | |
775 | 254 | final int yShift = cHeight + ypos + SEQS_ANNOTATION_GAP; |
776 | 254 | g.translate(0, yShift); |
777 | 254 | if (annotations == null) |
778 | { | |
779 | 0 | annotations = new AnnotationPanel(av); |
780 | } | |
781 | ||
782 | 254 | annotations.renderer.drawComponent(annotations, av, g, -1, |
783 | startColumn, endx + 1); | |
784 | 254 | g.translate(0, -yShift); |
785 | } | |
786 | 254 | g.translate(-xOffset, 0); |
787 | } | |
788 | ||
789 | /** | |
790 | * Draws scales left, right and above (if shown), and any hidden column | |
791 | * markers, on all widths of the wrapped alignment | |
792 | * | |
793 | * @param g | |
794 | * @param startColumn | |
795 | */ | |
796 | 254 | protected void drawWrappedDecorators(Graphics g, final int startColumn) |
797 | { | |
798 | 254 | int charWidth = av.getCharWidth(); |
799 | ||
800 | 254 | g.setFont(av.getFont()); |
801 | ||
802 | 254 | g.setColor(Color.black); |
803 | ||
804 | 254 | int ypos = wrappedSpaceAboveAlignment; |
805 | 254 | ViewportRanges ranges = av.getRanges(); |
806 | 254 | int viewportWidth = ranges.getViewportWidth(); |
807 | 254 | int maxWidth = ranges.getVisibleAlignmentWidth(); |
808 | 254 | int widthsDrawn = 0; |
809 | 254 | int startCol = startColumn; |
810 | ||
811 | 508 | while (widthsDrawn < wrappedVisibleWidths) |
812 | { | |
813 | 254 | int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1); |
814 | ||
815 | 254 | if (av.getScaleLeftWrapped()) |
816 | { | |
817 | 254 | drawVerticalScale(g, startCol, endColumn - 1, ypos, true); |
818 | } | |
819 | ||
820 | 254 | if (av.getScaleRightWrapped()) |
821 | { | |
822 | 254 | int x = labelWidthWest + viewportWidth * charWidth; |
823 | ||
824 | 254 | g.translate(x, 0); |
825 | 254 | drawVerticalScale(g, startCol, endColumn, ypos, false); |
826 | 254 | g.translate(-x, 0); |
827 | } | |
828 | ||
829 | /* | |
830 | * white fill region of scale above and hidden column markers | |
831 | * (to support incremental fast paint of image) | |
832 | */ | |
833 | 254 | g.translate(labelWidthWest, 0); |
834 | 254 | g.setColor(Color.white); |
835 | 254 | g.fillRect(0, ypos - wrappedSpaceAboveAlignment, |
836 | viewportWidth * charWidth + labelWidthWest, | |
837 | wrappedSpaceAboveAlignment); | |
838 | 254 | g.setColor(Color.black); |
839 | 254 | g.translate(-labelWidthWest, 0); |
840 | ||
841 | 254 | g.translate(labelWidthWest, 0); |
842 | ||
843 | 254 | if (av.getScaleAboveWrapped()) |
844 | { | |
845 | 0 | drawNorthScale(g, startCol, endColumn, ypos); |
846 | } | |
847 | ||
848 | 254 | if (av.hasHiddenColumns() && av.getShowHiddenMarkers()) |
849 | { | |
850 | 250 | drawHiddenColumnMarkers(g, ypos, startCol, endColumn); |
851 | } | |
852 | ||
853 | 254 | g.translate(-labelWidthWest, 0); |
854 | ||
855 | 254 | ypos += wrappedRepeatHeightPx; |
856 | 254 | startCol += viewportWidth; |
857 | 254 | widthsDrawn++; |
858 | } | |
859 | } | |
860 | ||
861 | /** | |
862 | * Draws markers (triangles) above hidden column positions between startColumn | |
863 | * and endColumn. | |
864 | * | |
865 | * @param g | |
866 | * @param ypos | |
867 | * @param startColumn | |
868 | * @param endColumn | |
869 | */ | |
870 | 250 | protected void drawHiddenColumnMarkers(Graphics g, int ypos, |
871 | int startColumn, int endColumn) | |
872 | { | |
873 | 250 | int charHeight = av.getCharHeight(); |
874 | 250 | int charWidth = av.getCharWidth(); |
875 | ||
876 | 250 | g.setColor(Color.blue); |
877 | 250 | int res; |
878 | 250 | HiddenColumns hidden = av.getAlignment().getHiddenColumns(); |
879 | ||
880 | 250 | Iterator<Integer> it = hidden.getStartRegionIterator(startColumn, |
881 | endColumn); | |
882 | 500 | while (it.hasNext()) |
883 | { | |
884 | 250 | res = it.next() - startColumn; |
885 | ||
886 | 250 | if (res < 0 || res > endColumn - startColumn + 1) |
887 | { | |
888 | 0 | continue; |
889 | } | |
890 | ||
891 | /* | |
892 | * draw a downward-pointing triangle at the hidden columns location | |
893 | * (before the following visible column) | |
894 | */ | |
895 | 250 | int xMiddle = res * charWidth; |
896 | 250 | int[] xPoints = new int[] { xMiddle - charHeight / 4, |
897 | xMiddle + charHeight / 4, xMiddle }; | |
898 | 250 | int yTop = ypos - (charHeight / 2); |
899 | 250 | int[] yPoints = new int[] { yTop, yTop, yTop + 8 }; |
900 | 250 | g.fillPolygon(xPoints, yPoints, 3); |
901 | } | |
902 | } | |
903 | ||
904 | /* | |
905 | * Draw a selection group over a wrapped alignment | |
906 | */ | |
907 | 0 | private void drawWrappedSelection(Graphics2D g, SequenceGroup group, |
908 | int canvasWidth, int canvasHeight, int startRes) | |
909 | { | |
910 | // chop the wrapped alignment extent up into panel-sized blocks and treat | |
911 | // each block as if it were a block from an unwrapped alignment | |
912 | 0 | g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, |
913 | BasicStroke.JOIN_ROUND, 3f, new float[] | |
914 | { 5f, 3f }, 0f)); | |
915 | 0 | g.setColor(Color.RED); |
916 | ||
917 | 0 | int charWidth = av.getCharWidth(); |
918 | 0 | int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) |
919 | / charWidth; | |
920 | 0 | int startx = startRes; |
921 | 0 | int maxwidth = av.getAlignment().getVisibleWidth(); |
922 | 0 | int ypos = wrappedSpaceAboveAlignment; |
923 | ||
924 | 0 | while ((ypos <= canvasHeight) && (startx < maxwidth)) |
925 | { | |
926 | // set end value to be start + width, or maxwidth, whichever is smaller | |
927 | 0 | int endx = startx + cWidth - 1; |
928 | ||
929 | 0 | if (endx > maxwidth) |
930 | { | |
931 | 0 | endx = maxwidth; |
932 | } | |
933 | ||
934 | 0 | g.translate(labelWidthWest, 0); |
935 | 0 | drawUnwrappedSelection(g, group, startx, endx, 0, |
936 | av.getAlignment().getHeight() - 1, ypos); | |
937 | 0 | g.translate(-labelWidthWest, 0); |
938 | ||
939 | 0 | ypos += wrappedRepeatHeightPx; |
940 | ||
941 | 0 | startx += cWidth; |
942 | } | |
943 | 0 | g.setStroke(new BasicStroke()); |
944 | } | |
945 | ||
946 | /** | |
947 | * Answers zero if annotations are not shown, otherwise recalculates and | |
948 | * answers the total height of all annotation rows in pixels | |
949 | * | |
950 | * @return | |
951 | */ | |
952 | 628 | int getAnnotationHeight() |
953 | { | |
954 | 628 | if (!av.isShowAnnotation()) |
955 | { | |
956 | 15 | return 0; |
957 | } | |
958 | ||
959 | 613 | if (annotations == null) |
960 | { | |
961 | 13 | annotations = new AnnotationPanel(av); |
962 | } | |
963 | ||
964 | 613 | return annotations.adjustPanelHeight(); |
965 | } | |
966 | ||
967 | /** | |
968 | * Draws the visible region of the alignment on the graphics context. If there | |
969 | * are hidden column markers in the visible region, then each sub-region | |
970 | * between the markers is drawn separately, followed by the hidden column | |
971 | * marker. | |
972 | * | |
973 | * @param localSeqR | |
974 | * - sequence renderer implementation - when null, uses the one used | |
975 | * for rendering interactive GUI | |
976 | * | |
977 | * @param g1 | |
978 | * the graphics context, positioned at the first residue to be drawn | |
979 | * @param startRes | |
980 | * offset of the first column to draw (0..) | |
981 | * @param endRes | |
982 | * offset of the last column to draw (0..) | |
983 | * @param startSeq | |
984 | * offset of the first sequence to draw (0..) | |
985 | * @param endSeq | |
986 | * offset of the last sequence to draw (0..) | |
987 | * @param yOffset | |
988 | * vertical offset at which to draw (for wrapped alignments) | |
989 | */ | |
990 | 2659 | public void drawPanel(SequenceRenderer localSeqR, Graphics g1, |
991 | final int startRes, final int endRes, final int startSeq, | |
992 | final int endSeq, final int yOffset) | |
993 | { | |
994 | 2659 | int charHeight = av.getCharHeight(); |
995 | 2659 | int charWidth = av.getCharWidth(); |
996 | ||
997 | 2659 | if (localSeqR == null) |
998 | { | |
999 | 0 | localSeqR = seqRdr; |
1000 | } | |
1001 | 2659 | if (!av.hasHiddenColumns()) |
1002 | { | |
1003 | 2110 | draw(localSeqR, g1, startRes, endRes, startSeq, endSeq, yOffset); |
1004 | } | |
1005 | else | |
1006 | { | |
1007 | 549 | int screenY = 0; |
1008 | 549 | int blockStart; |
1009 | 549 | int blockEnd; |
1010 | ||
1011 | 549 | HiddenColumns hidden = av.getAlignment().getHiddenColumns(); |
1012 | 549 | VisibleContigsIterator regions = hidden |
1013 | .getVisContigsIterator(startRes, endRes + 1, true); | |
1014 | ||
1015 | 1154 | while (regions.hasNext()) |
1016 | { | |
1017 | 605 | int[] region = regions.next(); |
1018 | 605 | blockEnd = region[1]; |
1019 | 605 | blockStart = region[0]; |
1020 | ||
1021 | /* | |
1022 | * draw up to just before the next hidden region, or the end of | |
1023 | * the visible region, whichever comes first | |
1024 | */ | |
1025 | 605 | g1.translate(screenY * charWidth, 0); |
1026 | ||
1027 | 605 | draw(localSeqR, g1, blockStart, blockEnd, startSeq, endSeq, |
1028 | yOffset); | |
1029 | ||
1030 | /* | |
1031 | * draw the downline of the hidden column marker (ScalePanel draws the | |
1032 | * triangle on top) if we reached it | |
1033 | */ | |
1034 | 605 | if (av.getShowHiddenMarkers() |
1035 | && (regions.hasNext() || regions.endsAtHidden())) | |
1036 | { | |
1037 | 56 | g1.setColor(Color.blue); |
1038 | ||
1039 | 56 | g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1, |
1040 | 0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1, | |
1041 | (endSeq - startSeq + 1) * charHeight + yOffset); | |
1042 | } | |
1043 | ||
1044 | 605 | g1.translate(-screenY * charWidth, 0); |
1045 | 605 | screenY += blockEnd - blockStart + 1; |
1046 | } | |
1047 | } | |
1048 | ||
1049 | } | |
1050 | ||
1051 | /** | |
1052 | * Draws a region of the visible alignment | |
1053 | * | |
1054 | * @param seqRdr | |
1055 | * | |
1056 | * @param g1 | |
1057 | * @param startRes | |
1058 | * offset of the first column in the visible region (0..) | |
1059 | * @param endRes | |
1060 | * offset of the last column in the visible region (0..) | |
1061 | * @param startSeq | |
1062 | * offset of the first sequence in the visible region (0..) | |
1063 | * @param endSeq | |
1064 | * offset of the last sequence in the visible region (0..) | |
1065 | * @param yOffset | |
1066 | * vertical offset at which to draw (for wrapped alignments) | |
1067 | */ | |
1068 | 2715 | private void draw(SequenceRenderer seqRdr, Graphics g, int startRes, |
1069 | int endRes, int startSeq, int endSeq, int offset) | |
1070 | { | |
1071 | 2715 | int charHeight = av.getCharHeight(); |
1072 | 2715 | int charWidth = av.getCharWidth(); |
1073 | ||
1074 | 2715 | g.setFont(av.getFont()); |
1075 | 2715 | seqRdr.prepare(g, av.isRenderGaps()); |
1076 | ||
1077 | 2715 | SequenceI nextSeq; |
1078 | ||
1079 | // / First draw the sequences | |
1080 | // /////////////////////////// | |
1081 | 15672 | for (int i = startSeq; i <= endSeq; i++) |
1082 | { | |
1083 | 12957 | nextSeq = av.getAlignment().getSequenceAt(i); |
1084 | 12957 | if (nextSeq == null) |
1085 | { | |
1086 | // occasionally, a race condition occurs such that the alignment row is | |
1087 | // empty | |
1088 | 0 | continue; |
1089 | } | |
1090 | 12957 | seqRdr.drawSequence(nextSeq, av.getAlignment().findAllGroups(nextSeq), |
1091 | startRes, endRes, offset + ((i - startSeq) * charHeight)); | |
1092 | ||
1093 | 12957 | if (av.isShowSequenceFeatures()) |
1094 | { | |
1095 | 1094 | fr.drawSequence(g, nextSeq, startRes, endRes, |
1096 | offset + ((i - startSeq) * charHeight), false); | |
1097 | } | |
1098 | ||
1099 | /* | |
1100 | * highlight search Results once sequence has been drawn | |
1101 | */ | |
1102 | 12957 | if (av.hasSearchResults()) |
1103 | { | |
1104 | 34 | SearchResultsI searchResults = av.getSearchResults(); |
1105 | 34 | int[] visibleResults = searchResults.getResults(nextSeq, startRes, |
1106 | endRes); | |
1107 | 34 | if (visibleResults != null) |
1108 | { | |
1109 | 24 | for (int r = 0; r < visibleResults.length; r += 2) |
1110 | { | |
1111 | 12 | seqRdr.drawHighlightedText(nextSeq, visibleResults[r], |
1112 | visibleResults[r + 1], | |
1113 | (visibleResults[r] - startRes) * charWidth, | |
1114 | offset + ((i - startSeq) * charHeight)); | |
1115 | } | |
1116 | } | |
1117 | } | |
1118 | } | |
1119 | ||
1120 | 2715 | if (av.getSelectionGroup() != null |
1121 | || av.getAlignment().getGroups().size() > 0) | |
1122 | { | |
1123 | 1139 | drawGroupsBoundaries(g, startRes, endRes, startSeq, endSeq, offset); |
1124 | } | |
1125 | ||
1126 | } | |
1127 | ||
1128 | /** | |
1129 | * Draws the outlines of any groups defined on the alignment (excluding the | |
1130 | * current selection group, if any) | |
1131 | * | |
1132 | * @param g1 | |
1133 | * @param startRes | |
1134 | * @param endRes | |
1135 | * @param startSeq | |
1136 | * @param endSeq | |
1137 | * @param offset | |
1138 | */ | |
1139 | 1139 | void drawGroupsBoundaries(Graphics g1, int startRes, int endRes, |
1140 | int startSeq, int endSeq, int offset) | |
1141 | { | |
1142 | 1139 | Graphics2D g = (Graphics2D) g1; |
1143 | ||
1144 | 1139 | SequenceGroup group = null; |
1145 | 1139 | int groupIndex = -1; |
1146 | ||
1147 | 1139 | if (av.getAlignment().getGroups().size() > 0) |
1148 | { | |
1149 | 1130 | group = av.getAlignment().getGroups().get(0); |
1150 | 1130 | groupIndex = 0; |
1151 | } | |
1152 | ||
1153 | 1139 | if (group != null) |
1154 | { | |
1155 | 1130 | do |
1156 | { | |
1157 | 1578 | g.setColor(group.getOutlineColour()); |
1158 | 1578 | drawPartialGroupOutline(g, group, startRes, endRes, startSeq, |
1159 | endSeq, offset); | |
1160 | ||
1161 | 1578 | groupIndex++; |
1162 | 1578 | if (groupIndex >= av.getAlignment().getGroups().size()) |
1163 | { | |
1164 | 1130 | break; |
1165 | } | |
1166 | 448 | group = av.getAlignment().getGroups().get(groupIndex); |
1167 | 448 | } while (groupIndex < av.getAlignment().getGroups().size()); |
1168 | } | |
1169 | } | |
1170 | ||
1171 | /** | |
1172 | * Draws the outline of the current selection group (if any) | |
1173 | * | |
1174 | * @param g | |
1175 | * @param startRes | |
1176 | * @param endRes | |
1177 | * @param startSeq | |
1178 | * @param endSeq | |
1179 | */ | |
1180 | 2892 | private void drawSelectionGroup(Graphics2D g, int startRes, int endRes, |
1181 | int startSeq, int endSeq) | |
1182 | { | |
1183 | 2892 | SequenceGroup group = av.getSelectionGroup(); |
1184 | 2892 | if (group == null) |
1185 | { | |
1186 | 2841 | return; |
1187 | } | |
1188 | ||
1189 | 51 | g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, |
1190 | BasicStroke.JOIN_ROUND, 3f, new float[] | |
1191 | { 5f, 3f }, 0f)); | |
1192 | 51 | g.setColor(Color.RED); |
1193 | 51 | if (!av.getWrapAlignment()) |
1194 | { | |
1195 | 51 | drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq, |
1196 | 0); | |
1197 | } | |
1198 | else | |
1199 | { | |
1200 | 0 | drawWrappedSelection(g, group, getWidth(), getHeight(), |
1201 | av.getRanges().getStartRes()); | |
1202 | } | |
1203 | 51 | g.setStroke(new BasicStroke()); |
1204 | } | |
1205 | ||
1206 | /** | |
1207 | * Draw the cursor as a separate image and overlay | |
1208 | * | |
1209 | * @param startRes | |
1210 | * start residue of area to draw cursor in | |
1211 | * @param endRes | |
1212 | * end residue of area to draw cursor in | |
1213 | * @param startSeq | |
1214 | * start sequence of area to draw cursor in | |
1215 | * @param endSeq | |
1216 | * end sequence of are to draw cursor in | |
1217 | * @return a transparent image of the same size as the sequence canvas, with | |
1218 | * the cursor drawn on it, if any | |
1219 | */ | |
1220 | 0 | private void drawCursor(Graphics g, int startRes, int endRes, |
1221 | int startSeq, int endSeq) | |
1222 | { | |
1223 | // convert the cursorY into a position on the visible alignment | |
1224 | 0 | int cursor_ypos = cursorY; |
1225 | ||
1226 | // don't do work unless we have to | |
1227 | 0 | if (cursor_ypos >= startSeq && cursor_ypos <= endSeq) |
1228 | { | |
1229 | 0 | int yoffset = 0; |
1230 | 0 | int xoffset = 0; |
1231 | 0 | int startx = startRes; |
1232 | 0 | int endx = endRes; |
1233 | ||
1234 | // convert the cursorX into a position on the visible alignment | |
1235 | 0 | int cursor_xpos = av.getAlignment().getHiddenColumns() |
1236 | .absoluteToVisibleColumn(cursorX); | |
1237 | ||
1238 | 0 | if (av.getAlignment().getHiddenColumns().isVisible(cursorX)) |
1239 | { | |
1240 | ||
1241 | 0 | if (av.getWrapAlignment()) |
1242 | { | |
1243 | // work out the correct offsets for the cursor | |
1244 | 0 | int charHeight = av.getCharHeight(); |
1245 | 0 | int charWidth = av.getCharWidth(); |
1246 | 0 | int canvasWidth = getWidth(); |
1247 | 0 | int canvasHeight = getHeight(); |
1248 | ||
1249 | // height gap above each panel | |
1250 | 0 | int hgap = charHeight; |
1251 | 0 | if (av.getScaleAboveWrapped()) |
1252 | { | |
1253 | 0 | hgap += charHeight; |
1254 | } | |
1255 | ||
1256 | 0 | int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) |
1257 | / charWidth; | |
1258 | 0 | int cHeight = av.getAlignment().getHeight() * charHeight; |
1259 | ||
1260 | 0 | endx = startx + cWidth - 1; |
1261 | 0 | int ypos = hgap; // vertical offset |
1262 | ||
1263 | // iterate down the wrapped panels | |
1264 | 0 | while ((ypos <= canvasHeight) && (endx < cursor_xpos)) |
1265 | { | |
1266 | // update vertical offset | |
1267 | 0 | ypos += cHeight + getAnnotationHeight() + hgap; |
1268 | ||
1269 | // update horizontal offset | |
1270 | 0 | startx += cWidth; |
1271 | 0 | endx = startx + cWidth - 1; |
1272 | } | |
1273 | 0 | yoffset = ypos; |
1274 | 0 | xoffset = labelWidthWest; |
1275 | } | |
1276 | ||
1277 | // now check if cursor is within range for x values | |
1278 | 0 | if (cursor_xpos >= startx && cursor_xpos <= endx) |
1279 | { | |
1280 | // get the character the cursor is drawn at | |
1281 | 0 | SequenceI seq = av.getAlignment().getSequenceAt(cursorY); |
1282 | 0 | char s = seq.getCharAt(cursorX); |
1283 | ||
1284 | 0 | seqRdr.drawCursor(g, s, |
1285 | xoffset + (cursor_xpos - startx) * av.getCharWidth(), | |
1286 | yoffset + (cursor_ypos - startSeq) * av.getCharHeight()); | |
1287 | } | |
1288 | } | |
1289 | } | |
1290 | } | |
1291 | ||
1292 | /** | |
1293 | * Draw a selection group over an unwrapped alignment | |
1294 | * | |
1295 | * @param g | |
1296 | * graphics object to draw with | |
1297 | * @param group | |
1298 | * selection group | |
1299 | * @param startRes | |
1300 | * start residue of area to draw | |
1301 | * @param endRes | |
1302 | * end residue of area to draw | |
1303 | * @param startSeq | |
1304 | * start sequence of area to draw | |
1305 | * @param endSeq | |
1306 | * end sequence of area to draw | |
1307 | * @param offset | |
1308 | * vertical offset (used when called from wrapped alignment code) | |
1309 | */ | |
1310 | 51 | private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group, |
1311 | int startRes, int endRes, int startSeq, int endSeq, int offset) | |
1312 | { | |
1313 | 51 | int charWidth = av.getCharWidth(); |
1314 | ||
1315 | 51 | if (!av.hasHiddenColumns()) |
1316 | { | |
1317 | 51 | drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq, |
1318 | offset); | |
1319 | } | |
1320 | else | |
1321 | { | |
1322 | // package into blocks of visible columns | |
1323 | 0 | int screenY = 0; |
1324 | 0 | int blockStart; |
1325 | 0 | int blockEnd; |
1326 | ||
1327 | 0 | HiddenColumns hidden = av.getAlignment().getHiddenColumns(); |
1328 | 0 | VisibleContigsIterator regions = hidden |
1329 | .getVisContigsIterator(startRes, endRes + 1, true); | |
1330 | 0 | while (regions.hasNext()) |
1331 | { | |
1332 | 0 | int[] region = regions.next(); |
1333 | 0 | blockEnd = region[1]; |
1334 | 0 | blockStart = region[0]; |
1335 | ||
1336 | 0 | g.translate(screenY * charWidth, 0); |
1337 | 0 | drawPartialGroupOutline(g, group, blockStart, blockEnd, startSeq, |
1338 | endSeq, offset); | |
1339 | ||
1340 | 0 | g.translate(-screenY * charWidth, 0); |
1341 | 0 | screenY += blockEnd - blockStart + 1; |
1342 | } | |
1343 | } | |
1344 | } | |
1345 | ||
1346 | /** | |
1347 | * Draws part of a selection group outline | |
1348 | * | |
1349 | * @param g | |
1350 | * @param group | |
1351 | * @param startRes | |
1352 | * @param endRes | |
1353 | * @param startSeq | |
1354 | * @param endSeq | |
1355 | * @param verticalOffset | |
1356 | */ | |
1357 | 1629 | private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group, |
1358 | int startRes, int endRes, int startSeq, int endSeq, | |
1359 | int verticalOffset) | |
1360 | { | |
1361 | 1629 | int charHeight = av.getCharHeight(); |
1362 | 1629 | int charWidth = av.getCharWidth(); |
1363 | 1629 | int visWidth = (endRes - startRes + 1) * charWidth; |
1364 | ||
1365 | 1629 | int oldY = -1; |
1366 | 1629 | int i = 0; |
1367 | 1629 | boolean inGroup = false; |
1368 | 1629 | int top = -1; |
1369 | 1629 | int bottom = -1; |
1370 | 1629 | int sy = -1; |
1371 | ||
1372 | 1629 | List<SequenceI> seqs = group.getSequences(null); |
1373 | ||
1374 | // position of start residue of group relative to startRes, in pixels | |
1375 | 1629 | int sx = (group.getStartRes() - startRes) * charWidth; |
1376 | ||
1377 | // width of group in pixels | |
1378 | 1629 | int xwidth = (((group.getEndRes() + 1) - group.getStartRes()) |
1379 | * charWidth) - 1; | |
1380 | ||
1381 | 1629 | if (!(sx + xwidth < 0 || sx > visWidth)) |
1382 | { | |
1383 | 9118 | for (i = startSeq; i <= endSeq; i++) |
1384 | { | |
1385 | 7489 | sy = verticalOffset + (i - startSeq) * charHeight; |
1386 | ||
1387 | 7489 | if ((sx <= (endRes - startRes) * charWidth) |
1388 | && seqs.contains(av.getAlignment().getSequenceAt(i))) | |
1389 | { | |
1390 | 4991 | if ((bottom == -1) |
1391 | && !seqs.contains(av.getAlignment().getSequenceAt(i + 1))) | |
1392 | { | |
1393 | 323 | bottom = sy + charHeight; |
1394 | } | |
1395 | ||
1396 | 4991 | if (!inGroup) |
1397 | { | |
1398 | 1190 | if (((top == -1) && (i == 0)) || !seqs |
1399 | .contains(av.getAlignment().getSequenceAt(i - 1))) | |
1400 | { | |
1401 | 1190 | top = sy; |
1402 | } | |
1403 | ||
1404 | 1190 | oldY = sy; |
1405 | 1190 | inGroup = true; |
1406 | } | |
1407 | } | |
1408 | 2498 | else if (inGroup) |
1409 | { | |
1410 | 108 | drawVerticals(g, sx, xwidth, visWidth, oldY, bottom); |
1411 | 108 | drawHorizontals(g, sx, xwidth, visWidth, top, bottom); |
1412 | ||
1413 | // reset top and bottom | |
1414 | 108 | top = -1; |
1415 | 108 | bottom = -1; |
1416 | 108 | inGroup = false; |
1417 | } | |
1418 | } | |
1419 | 1629 | if (inGroup) |
1420 | { | |
1421 | 1082 | sy = verticalOffset + ((i - startSeq) * charHeight); |
1422 | 1082 | drawVerticals(g, sx, xwidth, visWidth, oldY, |
1423 | 1082 | bottom == -1 ? sy : bottom); |
1424 | 1082 | drawHorizontals(g, sx, xwidth, visWidth, top, bottom); |
1425 | } | |
1426 | } | |
1427 | } | |
1428 | ||
1429 | /** | |
1430 | * Draw horizontal selection group boundaries at top and bottom positions | |
1431 | * | |
1432 | * @param g | |
1433 | * graphics object to draw on | |
1434 | * @param sx | |
1435 | * start x position | |
1436 | * @param xwidth | |
1437 | * width of gap | |
1438 | * @param visWidth | |
1439 | * visWidth maximum available width | |
1440 | * @param top | |
1441 | * position to draw top of group at | |
1442 | * @param bottom | |
1443 | * position to draw bottom of group at | |
1444 | */ | |
1445 | 1190 | private void drawHorizontals(Graphics2D g, int sx, int xwidth, |
1446 | int visWidth, int top, int bottom) | |
1447 | { | |
1448 | 1190 | int width = xwidth; |
1449 | 1190 | int startx = sx; |
1450 | 1190 | if (startx < 0) |
1451 | { | |
1452 | 343 | width += startx; |
1453 | 343 | startx = 0; |
1454 | } | |
1455 | ||
1456 | // don't let width extend beyond current block, or group extent | |
1457 | // fixes JAL-2672 | |
1458 | 1190 | if (startx + width >= visWidth) |
1459 | { | |
1460 | 1091 | width = visWidth - startx; |
1461 | } | |
1462 | ||
1463 | 1190 | if (top != -1) |
1464 | { | |
1465 | 1190 | g.drawLine(startx, top, startx + width, top); |
1466 | } | |
1467 | ||
1468 | 1190 | if (bottom != -1) |
1469 | { | |
1470 | 323 | g.drawLine(startx, bottom - 1, startx + width, bottom - 1); |
1471 | } | |
1472 | } | |
1473 | ||
1474 | /** | |
1475 | * Draw vertical lines at sx and sx+xwidth providing they lie within | |
1476 | * [0,visWidth) | |
1477 | * | |
1478 | * @param g | |
1479 | * graphics object to draw on | |
1480 | * @param sx | |
1481 | * start x position | |
1482 | * @param xwidth | |
1483 | * width of gap | |
1484 | * @param visWidth | |
1485 | * visWidth maximum available width | |
1486 | * @param oldY | |
1487 | * top y value | |
1488 | * @param sy | |
1489 | * bottom y value | |
1490 | */ | |
1491 | 1190 | private void drawVerticals(Graphics2D g, int sx, int xwidth, int visWidth, |
1492 | int oldY, int sy) | |
1493 | { | |
1494 | // if start position is visible, draw vertical line to left of | |
1495 | // group | |
1496 | 1190 | if (sx >= 0 && sx < visWidth) |
1497 | { | |
1498 | 847 | g.drawLine(sx, oldY, sx, sy - 1); |
1499 | } | |
1500 | ||
1501 | // if end position is visible, draw vertical line to right of | |
1502 | // group | |
1503 | 1190 | if (sx + xwidth < visWidth) |
1504 | { | |
1505 | 99 | g.drawLine(sx + xwidth, oldY, sx + xwidth, sy - 1); |
1506 | } | |
1507 | } | |
1508 | ||
1509 | /** | |
1510 | * Highlights search results in the visible region by rendering as white text | |
1511 | * on a black background. Any previous highlighting is removed. Answers true | |
1512 | * if any highlight was left on the visible alignment (so status bar should be | |
1513 | * set to match), else false. This method does _not_ set the 'fastPaint' flag, | |
1514 | * so allows the next repaint to update the whole display. | |
1515 | * | |
1516 | * @param results | |
1517 | * @return | |
1518 | */ | |
1519 | 0 | public boolean highlightSearchResults(SearchResultsI results) |
1520 | { | |
1521 | 0 | return highlightSearchResults(results, false); |
1522 | ||
1523 | } | |
1524 | ||
1525 | /** | |
1526 | * Highlights search results in the visible region by rendering as white text | |
1527 | * on a black background. Any previous highlighting is removed. Answers true | |
1528 | * if any highlight was left on the visible alignment (so status bar should be | |
1529 | * set to match), else false. | |
1530 | * <p> | |
1531 | * Optionally, set the 'fastPaint' flag for a faster redraw if only the | |
1532 | * highlighted regions are modified. This speeds up highlighting across linked | |
1533 | * alignments. | |
1534 | * <p> | |
1535 | * Currently fastPaint is not implemented for scrolled wrapped alignments. If | |
1536 | * a wrapped alignment had to be scrolled to show the highlighted region, then | |
1537 | * it should be fully redrawn, otherwise a fast paint can be performed. This | |
1538 | * argument could be removed if fast paint of scrolled wrapped alignment is | |
1539 | * coded in future (JAL-2609). | |
1540 | * | |
1541 | * @param results | |
1542 | * @param doFastPaint | |
1543 | * if true, sets a flag so the next repaint only redraws the modified | |
1544 | * image | |
1545 | * @return | |
1546 | */ | |
1547 | 2 | public boolean highlightSearchResults(SearchResultsI results, |
1548 | boolean doFastPaint) | |
1549 | { | |
1550 | 2 | if (fastpainting) |
1551 | { | |
1552 | 0 | return false; |
1553 | } | |
1554 | 2 | boolean wrapped = av.getWrapAlignment(); |
1555 | 2 | try |
1556 | { | |
1557 | 2 | fastPaint = doFastPaint; |
1558 | 2 | fastpainting = fastPaint; |
1559 | ||
1560 | /* | |
1561 | * to avoid redrawing the whole visible region, we instead | |
1562 | * redraw just the minimal regions to remove previous highlights | |
1563 | * and add new ones | |
1564 | */ | |
1565 | 2 | SearchResultsI previous = av.getSearchResults(); |
1566 | 2 | av.setSearchResults(results); |
1567 | 2 | boolean redrawn = false; |
1568 | 2 | boolean drawn = false; |
1569 | 2 | if (wrapped) |
1570 | { | |
1571 | 0 | redrawn = drawMappedPositionsWrapped(previous); |
1572 | 0 | drawn = drawMappedPositionsWrapped(results); |
1573 | 0 | redrawn |= drawn; |
1574 | } | |
1575 | else | |
1576 | { | |
1577 | 2 | redrawn = drawMappedPositions(previous); |
1578 | 2 | drawn = drawMappedPositions(results); |
1579 | 2 | redrawn |= drawn; |
1580 | } | |
1581 | ||
1582 | /* | |
1583 | * if highlights were either removed or added, repaint | |
1584 | */ | |
1585 | 2 | if (redrawn) |
1586 | { | |
1587 | 1 | repaint(); |
1588 | } | |
1589 | ||
1590 | /* | |
1591 | * return true only if highlights were added | |
1592 | */ | |
1593 | 2 | return drawn; |
1594 | ||
1595 | } finally | |
1596 | { | |
1597 | 2 | fastpainting = false; |
1598 | } | |
1599 | } | |
1600 | ||
1601 | /** | |
1602 | * Redraws the minimal rectangle in the visible region (if any) that includes | |
1603 | * mapped positions of the given search results. Whether or not positions are | |
1604 | * highlighted depends on the SearchResults set on the Viewport. This allows | |
1605 | * this method to be called to either clear or set highlighting. Answers true | |
1606 | * if any positions were drawn (in which case a repaint is still required), | |
1607 | * else false. | |
1608 | * | |
1609 | * @param results | |
1610 | * @return | |
1611 | */ | |
1612 | 4 | protected boolean drawMappedPositions(SearchResultsI results) |
1613 | { | |
1614 | 4 | if ((results == null) || (img == null)) // JAL-2784 check gg is not null |
1615 | { | |
1616 | 3 | return false; |
1617 | } | |
1618 | ||
1619 | /* | |
1620 | * calculate the minimal rectangle to redraw that | |
1621 | * includes both new and existing search results | |
1622 | */ | |
1623 | 1 | int firstSeq = Integer.MAX_VALUE; |
1624 | 1 | int lastSeq = -1; |
1625 | 1 | int firstCol = Integer.MAX_VALUE; |
1626 | 1 | int lastCol = -1; |
1627 | 1 | boolean matchFound = false; |
1628 | ||
1629 | 1 | ViewportRanges ranges = av.getRanges(); |
1630 | 1 | int firstVisibleColumn = ranges.getStartRes(); |
1631 | 1 | int lastVisibleColumn = ranges.getEndRes(); |
1632 | 1 | AlignmentI alignment = av.getAlignment(); |
1633 | 1 | if (av.hasHiddenColumns()) |
1634 | { | |
1635 | 0 | firstVisibleColumn = alignment.getHiddenColumns() |
1636 | .visibleToAbsoluteColumn(firstVisibleColumn); | |
1637 | 0 | lastVisibleColumn = alignment.getHiddenColumns() |
1638 | .visibleToAbsoluteColumn(lastVisibleColumn); | |
1639 | } | |
1640 | ||
1641 | 6 | for (int seqNo = ranges.getStartSeq(); seqNo <= ranges |
1642 | .getEndSeq(); seqNo++) | |
1643 | { | |
1644 | 5 | SequenceI seq = alignment.getSequenceAt(seqNo); |
1645 | ||
1646 | 5 | int[] visibleResults = results.getResults(seq, firstVisibleColumn, |
1647 | lastVisibleColumn); | |
1648 | 5 | if (visibleResults != null) |
1649 | { | |
1650 | 2 | for (int i = 0; i < visibleResults.length - 1; i += 2) |
1651 | { | |
1652 | 1 | int firstMatchedColumn = visibleResults[i]; |
1653 | 1 | int lastMatchedColumn = visibleResults[i + 1]; |
1654 | 1 | if (firstMatchedColumn <= lastVisibleColumn |
1655 | && lastMatchedColumn >= firstVisibleColumn) | |
1656 | { | |
1657 | /* | |
1658 | * found a search results match in the visible region - | |
1659 | * remember the first and last sequence matched, and the first | |
1660 | * and last visible columns in the matched positions | |
1661 | */ | |
1662 | 1 | matchFound = true; |
1663 | 1 | firstSeq = Math.min(firstSeq, seqNo); |
1664 | 1 | lastSeq = Math.max(lastSeq, seqNo); |
1665 | 1 | firstMatchedColumn = Math.max(firstMatchedColumn, |
1666 | firstVisibleColumn); | |
1667 | 1 | lastMatchedColumn = Math.min(lastMatchedColumn, |
1668 | lastVisibleColumn); | |
1669 | 1 | firstCol = Math.min(firstCol, firstMatchedColumn); |
1670 | 1 | lastCol = Math.max(lastCol, lastMatchedColumn); |
1671 | } | |
1672 | } | |
1673 | } | |
1674 | } | |
1675 | ||
1676 | 1 | if (matchFound) |
1677 | { | |
1678 | 1 | if (av.hasHiddenColumns()) |
1679 | { | |
1680 | 0 | firstCol = alignment.getHiddenColumns() |
1681 | .absoluteToVisibleColumn(firstCol); | |
1682 | 0 | lastCol = alignment.getHiddenColumns() |
1683 | .absoluteToVisibleColumn(lastCol); | |
1684 | } | |
1685 | 1 | int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth(); |
1686 | 1 | int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight(); |
1687 | 1 | Graphics gg = img.getGraphics(); |
1688 | 1 | gg.translate(transX, transY); |
1689 | 1 | drawPanel(seqRdr, gg, firstCol, lastCol, firstSeq, lastSeq, 0); |
1690 | 1 | gg.translate(-transX, -transY); |
1691 | 1 | gg.dispose(); |
1692 | } | |
1693 | ||
1694 | 1 | return matchFound; |
1695 | } | |
1696 | ||
1697 | 644 | @Override |
1698 | public void propertyChange(PropertyChangeEvent evt) | |
1699 | { | |
1700 | 644 | String eventName = evt.getPropertyName(); |
1701 | // jalview.bin.Console.errPrintln(">>SeqCanvas propertyChange " + | |
1702 | // eventName); | |
1703 | 644 | if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED)) |
1704 | { | |
1705 | 0 | fastPaint = true; |
1706 | 0 | repaint(); |
1707 | 0 | return; |
1708 | } | |
1709 | 644 | else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT)) |
1710 | { | |
1711 | 0 | fastPaint = false; |
1712 | // jalview.bin.Console.errPrintln("!!!! fastPaint false from | |
1713 | // MOVE_VIEWPORT"); | |
1714 | 0 | repaint(); |
1715 | 0 | return; |
1716 | } | |
1717 | ||
1718 | 644 | int scrollX = 0; |
1719 | 644 | if (eventName.equals(ViewportRanges.STARTRES) |
1720 | || eventName.equals(ViewportRanges.STARTRESANDSEQ)) | |
1721 | { | |
1722 | // Make sure we're not trying to draw a panel | |
1723 | // larger than the visible window | |
1724 | 36 | if (eventName.equals(ViewportRanges.STARTRES)) |
1725 | { | |
1726 | 36 | scrollX = (int) evt.getNewValue() - (int) evt.getOldValue(); |
1727 | } | |
1728 | else | |
1729 | { | |
1730 | 0 | scrollX = ((int[]) evt.getNewValue())[0] |
1731 | - ((int[]) evt.getOldValue())[0]; | |
1732 | } | |
1733 | 36 | ViewportRanges vpRanges = av.getRanges(); |
1734 | ||
1735 | 36 | int range = vpRanges.getEndRes() - vpRanges.getStartRes() + 1; |
1736 | 36 | if (scrollX > range) |
1737 | { | |
1738 | 2 | scrollX = range; |
1739 | } | |
1740 | 34 | else if (scrollX < -range) |
1741 | { | |
1742 | 0 | scrollX = -range; |
1743 | } | |
1744 | } | |
1745 | // Both scrolling and resizing change viewport ranges: scrolling changes | |
1746 | // both start and end points, but resize only changes end values. | |
1747 | // Here we only want to fastpaint on a scroll, with resize using a normal | |
1748 | // paint, so scroll events are identified as changes to the horizontal or | |
1749 | // vertical start value. | |
1750 | 644 | if (eventName.equals(ViewportRanges.STARTRES)) |
1751 | { | |
1752 | 36 | if (av.getWrapAlignment()) |
1753 | { | |
1754 | 1 | fastPaintWrapped(scrollX); |
1755 | } | |
1756 | else | |
1757 | { | |
1758 | 35 | fastPaint(scrollX, 0); |
1759 | } | |
1760 | } | |
1761 | 608 | else if (eventName.equals(ViewportRanges.STARTSEQ)) |
1762 | { | |
1763 | // scroll | |
1764 | 42 | fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue()); |
1765 | } | |
1766 | 566 | else if (eventName.equals(ViewportRanges.STARTRESANDSEQ)) |
1767 | { | |
1768 | 0 | if (av.getWrapAlignment()) |
1769 | { | |
1770 | 0 | fastPaintWrapped(scrollX); |
1771 | } | |
1772 | else | |
1773 | { | |
1774 | 0 | fastPaint(scrollX, 0); |
1775 | } | |
1776 | } | |
1777 | 566 | else if (eventName.equals(ViewportRanges.STARTSEQ)) |
1778 | { | |
1779 | // scroll | |
1780 | 0 | fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue()); |
1781 | } | |
1782 | 566 | else if (eventName.equals(ViewportRanges.STARTRESANDSEQ)) |
1783 | { | |
1784 | 0 | if (av.getWrapAlignment()) |
1785 | { | |
1786 | 0 | fastPaintWrapped(scrollX); |
1787 | } | |
1788 | } | |
1789 | } | |
1790 | ||
1791 | /** | |
1792 | * Does a minimal update of the image for a scroll movement. This method | |
1793 | * handles scroll movements of up to one width of the wrapped alignment (one | |
1794 | * click in the vertical scrollbar). Larger movements (for example after a | |
1795 | * scroll to highlight a mapped position) trigger a full redraw instead. | |
1796 | * | |
1797 | * @param scrollX | |
1798 | * number of positions scrolled (right if positive, left if negative) | |
1799 | */ | |
1800 | 1 | protected void fastPaintWrapped(int scrollX) |
1801 | { | |
1802 | 1 | ViewportRanges ranges = av.getRanges(); |
1803 | ||
1804 | 1 | if (Math.abs(scrollX) >= ranges.getViewportWidth()) |
1805 | { | |
1806 | /* | |
1807 | * shift of one view width or more is | |
1808 | * overcomplicated to handle in this method | |
1809 | */ | |
1810 | 1 | fastPaint = false; |
1811 | 1 | repaint(); |
1812 | 1 | return; |
1813 | } | |
1814 | ||
1815 | 0 | if (fastpainting || img == null) |
1816 | { | |
1817 | 0 | return; |
1818 | } | |
1819 | ||
1820 | 0 | fastPaint = true; |
1821 | 0 | fastpainting = true; |
1822 | ||
1823 | 0 | try |
1824 | { | |
1825 | ||
1826 | 0 | Graphics gg = img.getGraphics(); |
1827 | ||
1828 | 0 | calculateWrappedGeometry(getWidth(), getHeight()); |
1829 | ||
1830 | /* | |
1831 | * relocate the regions of the alignment that are still visible | |
1832 | */ | |
1833 | 0 | shiftWrappedAlignment(-scrollX); |
1834 | ||
1835 | /* | |
1836 | * add new columns (sequence, annotation) | |
1837 | * - at top left if scrollX < 0 | |
1838 | * - at right of last two widths if scrollX > 0 | |
1839 | */ | |
1840 | 0 | if (scrollX < 0) |
1841 | { | |
1842 | 0 | int startRes = ranges.getStartRes(); |
1843 | 0 | drawWrappedWidth(seqRdr, gg, wrappedSpaceAboveAlignment, startRes, |
1844 | startRes - scrollX - 1, getHeight()); | |
1845 | } | |
1846 | else | |
1847 | { | |
1848 | 0 | fastPaintWrappedAddRight(scrollX); |
1849 | } | |
1850 | ||
1851 | /* | |
1852 | * draw all scales (if shown) and hidden column markers | |
1853 | */ | |
1854 | 0 | drawWrappedDecorators(gg, ranges.getStartRes()); |
1855 | ||
1856 | 0 | gg.dispose(); |
1857 | ||
1858 | 0 | repaint(); |
1859 | } finally | |
1860 | { | |
1861 | 0 | fastpainting = false; |
1862 | } | |
1863 | } | |
1864 | ||
1865 | /** | |
1866 | * Draws the specified number of columns at the 'end' (bottom right) of a | |
1867 | * wrapped alignment view, including sequences and annotations if shown, but | |
1868 | * not scales. Also draws the same number of columns at the right hand end of | |
1869 | * the second last width shown, if the last width is not full height (so | |
1870 | * cannot simply be copied from the graphics image). | |
1871 | * | |
1872 | * @param columns | |
1873 | */ | |
1874 | 0 | protected void fastPaintWrappedAddRight(int columns) |
1875 | { | |
1876 | 0 | if (columns == 0) |
1877 | { | |
1878 | 0 | return; |
1879 | } | |
1880 | ||
1881 | 0 | Graphics gg = img.getGraphics(); |
1882 | ||
1883 | 0 | ViewportRanges ranges = av.getRanges(); |
1884 | 0 | int viewportWidth = ranges.getViewportWidth(); |
1885 | 0 | int charWidth = av.getCharWidth(); |
1886 | ||
1887 | /** | |
1888 | * draw full height alignment in the second last row, last columns, if the | |
1889 | * last row was not full height | |
1890 | */ | |
1891 | 0 | int visibleWidths = wrappedVisibleWidths; |
1892 | 0 | int canvasHeight = getHeight(); |
1893 | 0 | boolean lastWidthPartHeight = (wrappedVisibleWidths |
1894 | * wrappedRepeatHeightPx) > canvasHeight; | |
1895 | ||
1896 | 0 | if (lastWidthPartHeight) |
1897 | { | |
1898 | 0 | int widthsAbove = Math.max(0, visibleWidths - 2); |
1899 | 0 | int ypos = wrappedRepeatHeightPx * widthsAbove |
1900 | + wrappedSpaceAboveAlignment; | |
1901 | 0 | int endRes = ranges.getEndRes(); |
1902 | 0 | endRes += widthsAbove * viewportWidth; |
1903 | 0 | int startRes = endRes - columns; |
1904 | 0 | int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth) |
1905 | * charWidth; | |
1906 | ||
1907 | /* | |
1908 | * white fill first to erase annotations | |
1909 | */ | |
1910 | ||
1911 | 0 | gg.translate(xOffset, 0); |
1912 | 0 | gg.setColor(Color.white); |
1913 | 0 | gg.fillRect(labelWidthWest, ypos, (endRes - startRes + 1) * charWidth, |
1914 | wrappedRepeatHeightPx); | |
1915 | 0 | gg.translate(-xOffset, 0); |
1916 | ||
1917 | 0 | drawWrappedWidth(seqRdr, gg, ypos, startRes, endRes, canvasHeight); |
1918 | ||
1919 | } | |
1920 | ||
1921 | /* | |
1922 | * draw newly visible columns in last wrapped width (none if we | |
1923 | * have reached the end of the alignment) | |
1924 | * y-offset for drawing last width is height of widths above, | |
1925 | * plus one gap row | |
1926 | */ | |
1927 | 0 | int widthsAbove = visibleWidths - 1; |
1928 | 0 | int ypos = wrappedRepeatHeightPx * widthsAbove |
1929 | + wrappedSpaceAboveAlignment; | |
1930 | 0 | int endRes = ranges.getEndRes(); |
1931 | 0 | endRes += widthsAbove * viewportWidth; |
1932 | 0 | int startRes = endRes - columns + 1; |
1933 | ||
1934 | /* | |
1935 | * white fill first to erase annotations | |
1936 | */ | |
1937 | 0 | int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth) |
1938 | * charWidth; | |
1939 | 0 | gg.translate(xOffset, 0); |
1940 | 0 | gg.setColor(Color.white); |
1941 | 0 | int width = viewportWidth * charWidth - xOffset; |
1942 | 0 | gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx); |
1943 | 0 | gg.translate(-xOffset, 0); |
1944 | ||
1945 | 0 | gg.setFont(av.getFont()); |
1946 | 0 | gg.setColor(Color.black); |
1947 | ||
1948 | 0 | if (startRes < ranges.getVisibleAlignmentWidth()) |
1949 | { | |
1950 | 0 | drawWrappedWidth(seqRdr, gg, ypos, startRes, endRes, canvasHeight); |
1951 | } | |
1952 | ||
1953 | /* | |
1954 | * and finally, white fill any space below the visible alignment | |
1955 | */ | |
1956 | 0 | int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx; |
1957 | 0 | if (heightBelow > 0) |
1958 | { | |
1959 | 0 | gg.setColor(Color.white); |
1960 | 0 | gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow); |
1961 | } | |
1962 | 0 | gg.dispose(); |
1963 | } | |
1964 | ||
1965 | /** | |
1966 | * Shifts the visible alignment by the specified number of columns - left if | |
1967 | * negative, right if positive. Copies and moves sequences and annotations (if | |
1968 | * shown). Scales, hidden column markers and any newly visible columns must be | |
1969 | * drawn separately. | |
1970 | * | |
1971 | * @param positions | |
1972 | */ | |
1973 | 0 | protected void shiftWrappedAlignment(int positions) |
1974 | { | |
1975 | 0 | if (positions == 0) |
1976 | { | |
1977 | 0 | return; |
1978 | } | |
1979 | ||
1980 | 0 | Graphics gg = img.getGraphics(); |
1981 | ||
1982 | 0 | int charWidth = av.getCharWidth(); |
1983 | ||
1984 | 0 | int canvasHeight = getHeight(); |
1985 | 0 | ViewportRanges ranges = av.getRanges(); |
1986 | 0 | int viewportWidth = ranges.getViewportWidth(); |
1987 | 0 | int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions)) |
1988 | * charWidth; | |
1989 | 0 | int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment; |
1990 | 0 | int xMax = ranges.getVisibleAlignmentWidth(); |
1991 | ||
1992 | 0 | if (positions > 0) |
1993 | { | |
1994 | /* | |
1995 | * shift right (after scroll left) | |
1996 | * for each wrapped width (starting with the last), copy (width-positions) | |
1997 | * columns from the left margin to the right margin, and copy positions | |
1998 | * columns from the right margin of the row above (if any) to the | |
1999 | * left margin of the current row | |
2000 | */ | |
2001 | ||
2002 | /* | |
2003 | * get y-offset of last wrapped width, first row of sequences | |
2004 | */ | |
2005 | 0 | int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx; |
2006 | 0 | y += wrappedSpaceAboveAlignment; |
2007 | 0 | int copyFromLeftStart = labelWidthWest; |
2008 | 0 | int copyFromRightStart = copyFromLeftStart + widthToCopy; |
2009 | ||
2010 | 0 | while (y >= 0) |
2011 | { | |
2012 | /* | |
2013 | * shift 'widthToCopy' residues by 'positions' places to the right | |
2014 | */ | |
2015 | 0 | gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy, |
2016 | positions * charWidth, 0); | |
2017 | 0 | if (y > 0) |
2018 | { | |
2019 | /* | |
2020 | * copy 'positions' residue from the row above (right hand end) | |
2021 | * to this row's left hand end | |
2022 | */ | |
2023 | 0 | gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx, |
2024 | positions * charWidth, heightToCopy, -widthToCopy, | |
2025 | wrappedRepeatHeightPx); | |
2026 | } | |
2027 | ||
2028 | 0 | y -= wrappedRepeatHeightPx; |
2029 | } | |
2030 | } | |
2031 | else | |
2032 | { | |
2033 | /* | |
2034 | * shift left (after scroll right) | |
2035 | * for each wrapped width (starting with the first), copy (width-positions) | |
2036 | * columns from the right margin to the left margin, and copy positions | |
2037 | * columns from the left margin of the row below (if any) to the | |
2038 | * right margin of the current row | |
2039 | */ | |
2040 | 0 | int xpos = av.getRanges().getStartRes(); |
2041 | 0 | int y = wrappedSpaceAboveAlignment; |
2042 | 0 | int copyFromRightStart = labelWidthWest - positions * charWidth; |
2043 | ||
2044 | 0 | while (y < canvasHeight) |
2045 | { | |
2046 | 0 | gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy, |
2047 | positions * charWidth, 0); | |
2048 | 0 | if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx |
2049 | && (xpos + viewportWidth <= xMax)) | |
2050 | { | |
2051 | 0 | gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, |
2052 | -positions * charWidth, heightToCopy, widthToCopy, | |
2053 | -wrappedRepeatHeightPx); | |
2054 | } | |
2055 | 0 | y += wrappedRepeatHeightPx; |
2056 | 0 | xpos += viewportWidth; |
2057 | } | |
2058 | } | |
2059 | 0 | gg.dispose(); |
2060 | } | |
2061 | ||
2062 | /** | |
2063 | * Redraws any positions in the search results in the visible region of a | |
2064 | * wrapped alignment. Any highlights are drawn depending on the search results | |
2065 | * set on the Viewport, not the <code>results</code> argument. This allows | |
2066 | * this method to be called either to clear highlights (passing the previous | |
2067 | * search results), or to draw new highlights. | |
2068 | * | |
2069 | * @param results | |
2070 | * @return | |
2071 | */ | |
2072 | 0 | protected boolean drawMappedPositionsWrapped(SearchResultsI results) |
2073 | { | |
2074 | 0 | if ((results == null) || (img == null)) // JAL-2784 check gg is not null |
2075 | { | |
2076 | 0 | return false; |
2077 | } | |
2078 | 0 | int charHeight = av.getCharHeight(); |
2079 | ||
2080 | 0 | boolean matchFound = false; |
2081 | ||
2082 | 0 | calculateWrappedGeometry(getWidth(), getHeight()); |
2083 | 0 | int wrappedWidth = av.getWrappedWidth(); |
2084 | 0 | int wrappedHeight = wrappedRepeatHeightPx; |
2085 | ||
2086 | 0 | ViewportRanges ranges = av.getRanges(); |
2087 | 0 | int canvasHeight = getHeight(); |
2088 | 0 | int repeats = canvasHeight / wrappedHeight; |
2089 | 0 | if (canvasHeight / wrappedHeight > 0) |
2090 | { | |
2091 | 0 | repeats++; |
2092 | } | |
2093 | ||
2094 | 0 | int firstVisibleColumn = ranges.getStartRes(); |
2095 | 0 | int lastVisibleColumn = ranges.getStartRes() |
2096 | + repeats * ranges.getViewportWidth() - 1; | |
2097 | ||
2098 | 0 | AlignmentI alignment = av.getAlignment(); |
2099 | 0 | if (av.hasHiddenColumns()) |
2100 | { | |
2101 | 0 | firstVisibleColumn = alignment.getHiddenColumns() |
2102 | .visibleToAbsoluteColumn(firstVisibleColumn); | |
2103 | 0 | lastVisibleColumn = alignment.getHiddenColumns() |
2104 | .visibleToAbsoluteColumn(lastVisibleColumn); | |
2105 | } | |
2106 | ||
2107 | 0 | int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1); |
2108 | ||
2109 | 0 | Graphics gg = img.getGraphics(); |
2110 | ||
2111 | 0 | for (int seqNo = ranges.getStartSeq(); seqNo <= ranges |
2112 | .getEndSeq(); seqNo++) | |
2113 | { | |
2114 | 0 | SequenceI seq = alignment.getSequenceAt(seqNo); |
2115 | ||
2116 | 0 | int[] visibleResults = results.getResults(seq, firstVisibleColumn, |
2117 | lastVisibleColumn); | |
2118 | 0 | if (visibleResults != null) |
2119 | { | |
2120 | 0 | for (int i = 0; i < visibleResults.length - 1; i += 2) |
2121 | { | |
2122 | 0 | int firstMatchedColumn = visibleResults[i]; |
2123 | 0 | int lastMatchedColumn = visibleResults[i + 1]; |
2124 | 0 | if (firstMatchedColumn <= lastVisibleColumn |
2125 | && lastMatchedColumn >= firstVisibleColumn) | |
2126 | { | |
2127 | /* | |
2128 | * found a search results match in the visible region | |
2129 | */ | |
2130 | 0 | firstMatchedColumn = Math.max(firstMatchedColumn, |
2131 | firstVisibleColumn); | |
2132 | 0 | lastMatchedColumn = Math.min(lastMatchedColumn, |
2133 | lastVisibleColumn); | |
2134 | ||
2135 | /* | |
2136 | * draw each mapped position separately (as contiguous positions may | |
2137 | * wrap across lines) | |
2138 | */ | |
2139 | 0 | for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++) |
2140 | { | |
2141 | 0 | int displayColumn = mappedPos; |
2142 | 0 | if (av.hasHiddenColumns()) |
2143 | { | |
2144 | 0 | displayColumn = alignment.getHiddenColumns() |
2145 | .absoluteToVisibleColumn(displayColumn); | |
2146 | } | |
2147 | ||
2148 | /* | |
2149 | * transX: offset from left edge of canvas to residue position | |
2150 | */ | |
2151 | 0 | int transX = labelWidthWest |
2152 | + ((displayColumn - ranges.getStartRes()) | |
2153 | % wrappedWidth) * av.getCharWidth(); | |
2154 | ||
2155 | /* | |
2156 | * transY: offset from top edge of canvas to residue position | |
2157 | */ | |
2158 | 0 | int transY = gapHeight; |
2159 | 0 | transY += (displayColumn - ranges.getStartRes()) |
2160 | / wrappedWidth * wrappedHeight; | |
2161 | 0 | transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight(); |
2162 | ||
2163 | /* | |
2164 | * yOffset is from graphics origin to start of visible region | |
2165 | */ | |
2166 | 0 | int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight; |
2167 | 0 | if (transY < getHeight()) |
2168 | { | |
2169 | 0 | matchFound = true; |
2170 | 0 | gg.translate(transX, transY); |
2171 | 0 | drawPanel(seqRdr, gg, displayColumn, displayColumn, seqNo, |
2172 | seqNo, yOffset); | |
2173 | 0 | gg.translate(-transX, -transY); |
2174 | } | |
2175 | } | |
2176 | } | |
2177 | } | |
2178 | } | |
2179 | } | |
2180 | ||
2181 | 0 | gg.dispose(); |
2182 | ||
2183 | 0 | return matchFound; |
2184 | } | |
2185 | ||
2186 | /** | |
2187 | * Answers the width in pixels of the left scale labels (0 if not shown) | |
2188 | * | |
2189 | * @return | |
2190 | */ | |
2191 | 72 | int getLabelWidthWest() |
2192 | { | |
2193 | 72 | return labelWidthWest; |
2194 | } | |
2195 | ||
2196 | } |