Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
RotatableCanvas | 60 | 255 | 99 |
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.Color; | |
24 | import java.awt.Dimension; | |
25 | import java.awt.Font; | |
26 | import java.awt.Graphics; | |
27 | import java.awt.Graphics2D; | |
28 | import java.awt.Image; | |
29 | import java.awt.RenderingHints; | |
30 | import java.awt.event.InputEvent; | |
31 | import java.awt.event.KeyEvent; | |
32 | import java.awt.event.KeyListener; | |
33 | import java.awt.event.MouseEvent; | |
34 | import java.awt.event.MouseListener; | |
35 | import java.awt.event.MouseMotionListener; | |
36 | import java.awt.event.MouseWheelEvent; | |
37 | import java.awt.event.MouseWheelListener; | |
38 | import java.util.Arrays; | |
39 | import java.util.Iterator; | |
40 | import java.util.List; | |
41 | ||
42 | import javax.swing.JPanel; | |
43 | import javax.swing.ToolTipManager; | |
44 | ||
45 | import jalview.api.RotatableCanvasI; | |
46 | import jalview.datamodel.Point; | |
47 | import jalview.datamodel.SequenceGroup; | |
48 | import jalview.datamodel.SequenceI; | |
49 | import jalview.datamodel.SequencePoint; | |
50 | import jalview.math.RotatableMatrix; | |
51 | import jalview.math.RotatableMatrix.Axis; | |
52 | import jalview.util.ColorUtils; | |
53 | import jalview.util.MessageManager; | |
54 | import jalview.viewmodel.AlignmentViewport; | |
55 | ||
56 | /** | |
57 | * Models a Panel on which a set of points, and optionally x/y/z axes, can be | |
58 | * drawn, and rotated or zoomed with the mouse | |
59 | */ | |
60 | public class RotatableCanvas extends JPanel | |
61 | implements MouseListener, MouseMotionListener, KeyListener, | |
62 | RotatableCanvasI, MouseWheelListener | |
63 | { | |
64 | private static final float ZOOM_OUT = 0.9f; | |
65 | ||
66 | private static final float ZOOM_IN = 1.1f; | |
67 | ||
68 | /* | |
69 | * pixels distance within which tooltip shows sequence name | |
70 | */ | |
71 | private static final int NEARBY = 3; | |
72 | ||
73 | private static final List<String> AXES = Arrays.asList("x", "y", "z"); | |
74 | ||
75 | private static final Color AXIS_COLOUR = Color.yellow; | |
76 | ||
77 | private static final int DIMS = 3; | |
78 | ||
79 | boolean drawAxes = true; | |
80 | ||
81 | int mouseX; | |
82 | ||
83 | int mouseY; | |
84 | ||
85 | Image img; | |
86 | ||
87 | Graphics ig; | |
88 | ||
89 | Dimension prefSize; | |
90 | ||
91 | /* | |
92 | * the min-max [x, y, z] values of sequence points when the points | |
93 | * were set on the object, or when the view is reset; | |
94 | * x and y ranges are not recomputed as points are rotated, as this | |
95 | * would make scaling (zoom) unstable, but z ranges are (for correct | |
96 | * graduated colour brightness based on z-coordinate) | |
97 | */ | |
98 | float[] seqMin; | |
99 | ||
100 | float[] seqMax; | |
101 | ||
102 | /* | |
103 | * a scale factor used in drawing; when equal to 1, the points span | |
104 | * half the available width or height (whichever is less); increase this | |
105 | * factor to zoom in, decrease it to zoom out | |
106 | */ | |
107 | private float scaleFactor; | |
108 | ||
109 | int npoint; | |
110 | ||
111 | /* | |
112 | * sequences and their (x, y, z) PCA dimension values | |
113 | */ | |
114 | List<SequencePoint> sequencePoints; | |
115 | ||
116 | /* | |
117 | * x, y, z axis end points (PCA dimension values) | |
118 | */ | |
119 | private Point[] axisEndPoints; | |
120 | ||
121 | // fields for 'select rectangle' (JAL-1124) | |
122 | int rectx1; | |
123 | ||
124 | int recty1; | |
125 | ||
126 | int rectx2; | |
127 | ||
128 | int recty2; | |
129 | ||
130 | AlignmentViewport av; | |
131 | ||
132 | AlignmentPanel ap; | |
133 | ||
134 | private boolean showLabels; | |
135 | ||
136 | private Color bgColour; | |
137 | ||
138 | private boolean applyToAllViews; | |
139 | ||
140 | /** | |
141 | * Constructor | |
142 | * | |
143 | * @param panel | |
144 | */ | |
145 | 2 | public RotatableCanvas(AlignmentPanel panel) |
146 | { | |
147 | 2 | this.av = panel.av; |
148 | 2 | this.ap = panel; |
149 | 2 | setAxisEndPoints(new Point[DIMS]); |
150 | 2 | setShowLabels(false); |
151 | 2 | setApplyToAllViews(false); |
152 | 2 | setBgColour(Color.BLACK); |
153 | 2 | resetAxes(); |
154 | ||
155 | 2 | ToolTipManager.sharedInstance().registerComponent(this); |
156 | ||
157 | 2 | addMouseListener(this); |
158 | 2 | addMouseMotionListener(this); |
159 | 2 | addMouseWheelListener(this); |
160 | } | |
161 | ||
162 | /** | |
163 | * Refreshes the display with labels shown (or not) | |
164 | * | |
165 | * @param show | |
166 | */ | |
167 | 0 | public void showLabels(boolean show) |
168 | { | |
169 | 0 | setShowLabels(show); |
170 | 0 | repaint(); |
171 | } | |
172 | ||
173 | 2 | @Override |
174 | public void setPoints(List<SequencePoint> points, int np) | |
175 | { | |
176 | 2 | this.sequencePoints = points; |
177 | 2 | this.npoint = np; |
178 | 2 | prefSize = getPreferredSize(); |
179 | ||
180 | 2 | findWidths(); |
181 | ||
182 | 2 | setScaleFactor(1f); |
183 | } | |
184 | ||
185 | /** | |
186 | * Resets axes to the initial state: x-axis to the right, y-axis up, z-axis to | |
187 | * back (so obscured in a 2-D display) | |
188 | */ | |
189 | 2 | protected void resetAxes() |
190 | { | |
191 | 2 | getAxisEndPoints()[0] = new Point(1f, 0f, 0f); |
192 | 2 | getAxisEndPoints()[1] = new Point(0f, 1f, 0f); |
193 | 2 | getAxisEndPoints()[2] = new Point(0f, 0f, 1f); |
194 | } | |
195 | ||
196 | /** | |
197 | * Computes and saves the min-max ranges of x/y/z positions of the sequence | |
198 | * points | |
199 | */ | |
200 | 2 | protected void findWidths() |
201 | { | |
202 | 2 | float[] max = new float[DIMS]; |
203 | 2 | float[] min = new float[DIMS]; |
204 | ||
205 | 2 | max[0] = -Float.MAX_VALUE; |
206 | 2 | max[1] = -Float.MAX_VALUE; |
207 | 2 | max[2] = -Float.MAX_VALUE; |
208 | ||
209 | 2 | min[0] = Float.MAX_VALUE; |
210 | 2 | min[1] = Float.MAX_VALUE; |
211 | 2 | min[2] = Float.MAX_VALUE; |
212 | ||
213 | 2 | for (SequencePoint sp : sequencePoints) |
214 | { | |
215 | 30 | max[0] = Math.max(max[0], sp.coord.x); |
216 | 30 | max[1] = Math.max(max[1], sp.coord.y); |
217 | 30 | max[2] = Math.max(max[2], sp.coord.z); |
218 | 30 | min[0] = Math.min(min[0], sp.coord.x); |
219 | 30 | min[1] = Math.min(min[1], sp.coord.y); |
220 | 30 | min[2] = Math.min(min[2], sp.coord.z); |
221 | } | |
222 | ||
223 | 2 | seqMin = min; |
224 | 2 | seqMax = max; |
225 | } | |
226 | ||
227 | /** | |
228 | * Answers the preferred size if it has been set, else 400 x 400 | |
229 | * | |
230 | * @return | |
231 | */ | |
232 | 2 | @Override |
233 | public Dimension getPreferredSize() | |
234 | { | |
235 | 2 | if (prefSize != null) |
236 | { | |
237 | 0 | return prefSize; |
238 | } | |
239 | else | |
240 | { | |
241 | 2 | return new Dimension(400, 400); |
242 | } | |
243 | } | |
244 | ||
245 | /** | |
246 | * Answers the preferred size | |
247 | * | |
248 | * @return | |
249 | * @see RotatableCanvas#getPreferredSize() | |
250 | */ | |
251 | 0 | @Override |
252 | public Dimension getMinimumSize() | |
253 | { | |
254 | 0 | return getPreferredSize(); |
255 | } | |
256 | ||
257 | /** | |
258 | * Repaints the panel | |
259 | * | |
260 | * @param g | |
261 | */ | |
262 | 0 | @Override |
263 | public void paintComponent(Graphics g1) | |
264 | { | |
265 | ||
266 | 0 | Graphics2D g = (Graphics2D) g1; |
267 | ||
268 | 0 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
269 | RenderingHints.VALUE_ANTIALIAS_ON); | |
270 | 0 | if (sequencePoints == null) |
271 | { | |
272 | 0 | g.setFont(new Font("Verdana", Font.PLAIN, 18)); |
273 | 0 | g.drawString( |
274 | MessageManager.getString("label.calculating_pca") + "....", | |
275 | 20, getHeight() / 2); | |
276 | } | |
277 | else | |
278 | { | |
279 | /* | |
280 | * create the image at the beginning or after a resize | |
281 | */ | |
282 | 0 | boolean resized = prefSize.width != getWidth() |
283 | || prefSize.height != getHeight(); | |
284 | 0 | if (img == null || resized) |
285 | { | |
286 | 0 | prefSize.width = getWidth(); |
287 | 0 | prefSize.height = getHeight(); |
288 | ||
289 | 0 | img = createImage(getWidth(), getHeight()); |
290 | 0 | ig = img.getGraphics(); |
291 | } | |
292 | ||
293 | 0 | drawBackground(ig); |
294 | 0 | drawScene(ig); |
295 | ||
296 | 0 | if (drawAxes) |
297 | { | |
298 | 0 | drawAxes(ig); |
299 | } | |
300 | ||
301 | 0 | g.drawImage(img, 0, 0, this); |
302 | } | |
303 | } | |
304 | ||
305 | /** | |
306 | * Resets the rotation and choice of axes to the initial state (without change | |
307 | * of scale factor) | |
308 | */ | |
309 | 0 | public void resetView() |
310 | { | |
311 | 0 | img = null; |
312 | 0 | findWidths(); |
313 | 0 | resetAxes(); |
314 | 0 | repaint(); |
315 | } | |
316 | ||
317 | /** | |
318 | * Draws lines for the x, y, z axes | |
319 | * | |
320 | * @param g | |
321 | */ | |
322 | 0 | public void drawAxes(Graphics g) |
323 | { | |
324 | 0 | g.setColor(AXIS_COLOUR); |
325 | ||
326 | 0 | int midX = getWidth() / 2; |
327 | 0 | int midY = getHeight() / 2; |
328 | // float maxWidth = Math.max(Math.abs(seqMax[0] - seqMin[0]), | |
329 | // Math.abs(seqMax[1] - seqMin[1])); | |
330 | 0 | int pix = Math.min(getWidth(), getHeight()); |
331 | 0 | float scaleBy = pix * getScaleFactor() / (2f); |
332 | ||
333 | 0 | for (int i = 0; i < DIMS; i++) |
334 | { | |
335 | 0 | g.drawLine(midX, midY, |
336 | midX + (int) (getAxisEndPoints()[i].x * scaleBy * 0.25), | |
337 | midY + (int) (getAxisEndPoints()[i].y * scaleBy * 0.25)); | |
338 | } | |
339 | } | |
340 | ||
341 | /** | |
342 | * Fills the background with the currently configured background colour | |
343 | * | |
344 | * @param g | |
345 | */ | |
346 | 0 | public void drawBackground(Graphics g) |
347 | { | |
348 | 0 | g.setColor(getBgColour()); |
349 | 0 | g.fillRect(0, 0, prefSize.width, prefSize.height); |
350 | } | |
351 | ||
352 | /** | |
353 | * Draws points (6x6 squares) for the sequences of the PCA, and labels | |
354 | * (sequence names) if configured to do so. The sequence points colours are | |
355 | * taken from the sequence ids in the alignment (converting black to white). | |
356 | * Sequences 'at the back' (z-coordinate is negative) are shaded slightly | |
357 | * darker to help give a 3-D sensation. | |
358 | * | |
359 | * @param g | |
360 | */ | |
361 | 0 | public void drawScene(Graphics g1) |
362 | { | |
363 | 0 | Graphics2D g = (Graphics2D) g1; |
364 | ||
365 | 0 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
366 | RenderingHints.VALUE_ANTIALIAS_ON); | |
367 | 0 | int pix = Math.min(getWidth(), getHeight()); |
368 | 0 | float xWidth = Math.abs(seqMax[0] - seqMin[0]); |
369 | 0 | float yWidth = Math.abs(seqMax[1] - seqMin[1]); |
370 | 0 | float maxWidth = Math.max(xWidth, yWidth); |
371 | 0 | float scaleBy = pix * getScaleFactor() / (2f * maxWidth); |
372 | ||
373 | 0 | float[] centre = getCentre(); |
374 | ||
375 | 0 | for (int i = 0; i < npoint; i++) |
376 | { | |
377 | /* | |
378 | * sequence point colour as sequence id, but | |
379 | * gray if sequence is currently selected | |
380 | */ | |
381 | 0 | SequencePoint sp = sequencePoints.get(i); |
382 | 0 | Color sequenceColour = getSequencePointColour(sp); |
383 | 0 | g.setColor(sequenceColour); |
384 | ||
385 | 0 | int halfwidth = getWidth() / 2; |
386 | 0 | int halfheight = getHeight() / 2; |
387 | 0 | int x = (int) ((sp.coord.x - centre[0]) * scaleBy) + halfwidth; |
388 | 0 | int y = (int) ((sp.coord.y - centre[1]) * scaleBy) + halfheight; |
389 | 0 | g.fillRect(x - 3, y - 3, 6, 6); |
390 | ||
391 | 0 | if (isShowLabels()) |
392 | { | |
393 | 0 | g.setColor(Color.red); |
394 | 0 | g.drawString(sp.getSequence().getName(), x - 3, y - 4); |
395 | } | |
396 | } | |
397 | 0 | if (isShowLabels()) |
398 | { | |
399 | 0 | g.setColor(AXIS_COLOUR); |
400 | 0 | int midX = getWidth() / 2; |
401 | 0 | int midY = getHeight() / 2; |
402 | 0 | Iterator<String> axes = AXES.iterator(); |
403 | 0 | for (Point p : getAxisEndPoints()) |
404 | { | |
405 | 0 | int x = midX + (int) (p.x * scaleBy * seqMax[0]); |
406 | 0 | int y = midY + (int) (p.y * scaleBy * seqMax[1]); |
407 | 0 | g.drawString(axes.next(), x - 3, y - 4); |
408 | } | |
409 | } | |
410 | // //Now the rectangle | |
411 | 0 | if (rectx2 != -1 && recty2 != -1) |
412 | { | |
413 | 0 | g.setColor(Color.white); |
414 | ||
415 | 0 | g.drawRect(rectx1, recty1, rectx2 - rectx1, recty2 - recty1); |
416 | } | |
417 | } | |
418 | ||
419 | /** | |
420 | * Determines the colour to use when drawing a sequence point. The colour is | |
421 | * taken from the sequence id, with black converted to white, and then | |
422 | * graduated from darker (at the back) to brighter (at the front) based on the | |
423 | * z-axis coordinate of the point. | |
424 | * | |
425 | * @param sp | |
426 | * @return | |
427 | */ | |
428 | 0 | protected Color getSequencePointColour(SequencePoint sp) |
429 | { | |
430 | 0 | SequenceI sequence = sp.getSequence(); |
431 | 0 | Color sequenceColour = av.getSequenceColour(sequence); |
432 | 0 | if (sequenceColour == Color.black) |
433 | { | |
434 | 0 | sequenceColour = Color.white; |
435 | } | |
436 | 0 | if (av.getSelectionGroup() != null) |
437 | { | |
438 | 0 | if (av.getSelectionGroup().getSequences(null).contains(sequence)) |
439 | { | |
440 | 0 | sequenceColour = Color.gray; |
441 | } | |
442 | } | |
443 | ||
444 | /* | |
445 | * graduate brighter for point in front of centre, darker if behind centre | |
446 | */ | |
447 | 0 | float zCentre = (seqMin[2] + seqMax[2]) / 2f; |
448 | 0 | if (sp.coord.z > zCentre) |
449 | { | |
450 | 0 | sequenceColour = ColorUtils.getGraduatedColour(sp.coord.z, 0, |
451 | sequenceColour, seqMax[2], sequenceColour.brighter()); | |
452 | } | |
453 | 0 | else if (sp.coord.z < zCentre) |
454 | { | |
455 | 0 | sequenceColour = ColorUtils.getGraduatedColour(sp.coord.z, seqMin[2], |
456 | sequenceColour.darker(), 0, sequenceColour); | |
457 | } | |
458 | ||
459 | 0 | return sequenceColour; |
460 | } | |
461 | ||
462 | 0 | @Override |
463 | public void keyTyped(KeyEvent evt) | |
464 | { | |
465 | } | |
466 | ||
467 | 0 | @Override |
468 | public void keyReleased(KeyEvent evt) | |
469 | { | |
470 | } | |
471 | ||
472 | /** | |
473 | * Responds to up or down arrow key by zooming in or out, respectively | |
474 | * | |
475 | * @param evt | |
476 | */ | |
477 | 0 | @Override |
478 | public void keyPressed(KeyEvent evt) | |
479 | { | |
480 | 0 | int keyCode = evt.getKeyCode(); |
481 | 0 | boolean shiftDown = evt.isShiftDown(); |
482 | ||
483 | 0 | if (keyCode == KeyEvent.VK_UP) |
484 | { | |
485 | 0 | if (shiftDown) |
486 | { | |
487 | 0 | rotate(0f, -1f); |
488 | } | |
489 | else | |
490 | { | |
491 | 0 | zoom(ZOOM_IN); |
492 | } | |
493 | } | |
494 | 0 | else if (keyCode == KeyEvent.VK_DOWN) |
495 | { | |
496 | 0 | if (shiftDown) |
497 | { | |
498 | 0 | rotate(0f, 1f); |
499 | } | |
500 | else | |
501 | { | |
502 | 0 | zoom(ZOOM_OUT); |
503 | } | |
504 | } | |
505 | 0 | else if (shiftDown && keyCode == KeyEvent.VK_LEFT) |
506 | { | |
507 | 0 | rotate(1f, 0f); |
508 | } | |
509 | 0 | else if (shiftDown && keyCode == KeyEvent.VK_RIGHT) |
510 | { | |
511 | 0 | rotate(-1f, 0f); |
512 | } | |
513 | /* | |
514 | else if (evt.getKeyChar() == 's') | |
515 | { | |
516 | // Cache.warn("DEBUG: Rectangle selection"); | |
517 | // todo not yet enabled as rectx2, recty2 are always -1 | |
518 | // need to set them in mouseDragged; JAL-1124 | |
519 | if ((rectx2 != -1) && (recty2 != -1)) | |
520 | { | |
521 | rectSelect(rectx1, recty1, rectx2, recty2); | |
522 | } | |
523 | } | |
524 | */ | |
525 | ||
526 | 0 | repaint(); |
527 | } | |
528 | ||
529 | 0 | @Override |
530 | public void zoom(float factor) | |
531 | { | |
532 | 0 | if (factor > 0f) |
533 | { | |
534 | 0 | setScaleFactor(getScaleFactor() * factor); |
535 | } | |
536 | } | |
537 | ||
538 | 0 | @Override |
539 | public void mouseClicked(MouseEvent evt) | |
540 | { | |
541 | } | |
542 | ||
543 | 0 | @Override |
544 | public void mouseEntered(MouseEvent evt) | |
545 | { | |
546 | } | |
547 | ||
548 | 0 | @Override |
549 | public void mouseExited(MouseEvent evt) | |
550 | { | |
551 | } | |
552 | ||
553 | 0 | @Override |
554 | public void mouseReleased(MouseEvent evt) | |
555 | { | |
556 | } | |
557 | ||
558 | /** | |
559 | * If the mouse press is at (within 2 pixels of) a sequence point, toggles | |
560 | * (adds or removes) the corresponding sequence as a member of the viewport | |
561 | * selection group. This supports configuring a group in the alignment by | |
562 | * clicking on points in the PCA display. | |
563 | */ | |
564 | 0 | @Override |
565 | public void mousePressed(MouseEvent evt) | |
566 | { | |
567 | 0 | int x = evt.getX(); |
568 | 0 | int y = evt.getY(); |
569 | ||
570 | 0 | mouseX = x; |
571 | 0 | mouseY = y; |
572 | ||
573 | 0 | rectx1 = x; |
574 | 0 | recty1 = y; |
575 | 0 | rectx2 = -1; |
576 | 0 | recty2 = -1; |
577 | ||
578 | 0 | SequenceI found = findSequenceAtPoint(x, y); |
579 | ||
580 | 0 | if (found != null) |
581 | { | |
582 | 0 | AlignmentPanel[] aps = getAssociatedPanels(); |
583 | ||
584 | 0 | for (int a = 0; a < aps.length; a++) |
585 | { | |
586 | 0 | if (aps[a].av.getSelectionGroup() != null) |
587 | { | |
588 | 0 | aps[a].av.getSelectionGroup().addOrRemove(found, true); |
589 | } | |
590 | else | |
591 | { | |
592 | 0 | aps[a].av.setSelectionGroup(new SequenceGroup()); |
593 | 0 | aps[a].av.getSelectionGroup().addOrRemove(found, true); |
594 | 0 | aps[a].av.getSelectionGroup() |
595 | .setEndRes(aps[a].av.getAlignment().getWidth() - 1); | |
596 | } | |
597 | } | |
598 | 0 | PaintRefresher.Refresh(this, av.getSequenceSetId()); |
599 | // canonical selection is sent to other listeners | |
600 | 0 | av.sendSelection(); |
601 | } | |
602 | ||
603 | 0 | repaint(); |
604 | } | |
605 | ||
606 | /** | |
607 | * Sets the tooltip to the name of the sequence within 2 pixels of the mouse | |
608 | * position, or clears the tooltip if none found | |
609 | */ | |
610 | 0 | @Override |
611 | public void mouseMoved(MouseEvent evt) | |
612 | { | |
613 | 0 | SequenceI found = findSequenceAtPoint(evt.getX(), evt.getY()); |
614 | ||
615 | 0 | this.setToolTipText(found == null ? null : found.getName()); |
616 | } | |
617 | ||
618 | /** | |
619 | * Action handler for a mouse drag. Rotates the display around the X axis (for | |
620 | * up/down mouse movement) and/or the Y axis (for left/right mouse movement). | |
621 | * | |
622 | * @param evt | |
623 | */ | |
624 | 0 | @Override |
625 | public void mouseDragged(MouseEvent evt) | |
626 | { | |
627 | 0 | int xPos = evt.getX(); |
628 | 0 | int yPos = evt.getY(); |
629 | ||
630 | 0 | if (xPos == mouseX && yPos == mouseY) |
631 | { | |
632 | 0 | return; |
633 | } | |
634 | ||
635 | 0 | int xDelta = xPos - mouseX; |
636 | 0 | int yDelta = yPos - mouseY; |
637 | ||
638 | // Check if this is a rectangle drawing drag | |
639 | 0 | if ((evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0) |
640 | { | |
641 | 0 | rectx2 = evt.getX(); |
642 | 0 | recty2 = evt.getY(); |
643 | 0 | if ((rectx2 != -1) && (recty2 != -1)) |
644 | { | |
645 | 0 | rectSelect(rectx1, recty1, rectx2, recty2); |
646 | } | |
647 | } | |
648 | else | |
649 | { | |
650 | 0 | rotate(xDelta, yDelta); |
651 | ||
652 | 0 | mouseX = xPos; |
653 | 0 | mouseY = yPos; |
654 | // findWidths(); | |
655 | } | |
656 | 0 | repaint(); |
657 | } | |
658 | ||
659 | 0 | @Override |
660 | public void rotate(float x, float y) | |
661 | { | |
662 | 0 | if (x == 0f && y == 0f) |
663 | { | |
664 | 0 | return; |
665 | } | |
666 | ||
667 | /* | |
668 | * get the identity transformation... | |
669 | */ | |
670 | 0 | RotatableMatrix rotmat = new RotatableMatrix(); |
671 | ||
672 | /* | |
673 | * rotate around the X axis for change in Y | |
674 | * (mouse movement up/down); note we are equating a | |
675 | * number of pixels with degrees of rotation here! | |
676 | */ | |
677 | 0 | if (y != 0) |
678 | { | |
679 | 0 | rotmat.rotate(y, Axis.X); |
680 | } | |
681 | ||
682 | /* | |
683 | * rotate around the Y axis for change in X | |
684 | * (mouse movement left/right) | |
685 | */ | |
686 | 0 | if (x != 0) |
687 | { | |
688 | 0 | rotmat.rotate(x, Axis.Y); |
689 | } | |
690 | ||
691 | /* | |
692 | * apply the composite transformation to sequence points; | |
693 | * update z min-max range (affects colour graduation), but not | |
694 | * x or y min-max (as this would affect axis scaling) | |
695 | */ | |
696 | 0 | float[] centre = getCentre(); |
697 | 0 | float zMin = Float.MAX_VALUE; |
698 | 0 | float zMax = -Float.MAX_VALUE; |
699 | ||
700 | 0 | for (int i = 0; i < npoint; i++) |
701 | { | |
702 | 0 | SequencePoint sp = sequencePoints.get(i); |
703 | 0 | sp.translate(-centre[0], -centre[1], -centre[2]); |
704 | ||
705 | // Now apply the rotation matrix | |
706 | 0 | sp.coord = rotmat.vectorMultiply(sp.coord); |
707 | ||
708 | // Now translate back again | |
709 | 0 | sp.translate(centre[0], centre[1], centre[2]); |
710 | ||
711 | 0 | zMin = Math.min(zMin, sp.coord.z); |
712 | 0 | zMax = Math.max(zMax, sp.coord.z); |
713 | } | |
714 | ||
715 | 0 | seqMin[2] = zMin; |
716 | 0 | seqMax[2] = zMax; |
717 | ||
718 | /* | |
719 | * rotate the x/y/z axis positions | |
720 | */ | |
721 | 0 | for (int i = 0; i < DIMS; i++) |
722 | { | |
723 | 0 | getAxisEndPoints()[i] = rotmat.vectorMultiply(getAxisEndPoints()[i]); |
724 | } | |
725 | } | |
726 | ||
727 | /** | |
728 | * Answers the x/y/z coordinates that are midway between the maximum and | |
729 | * minimum sequence point values | |
730 | * | |
731 | * @return | |
732 | */ | |
733 | 0 | private float[] getCentre() |
734 | { | |
735 | 0 | float xCentre = (seqMin[0] + seqMax[0]) / 2f; |
736 | 0 | float yCentre = (seqMin[1] + seqMax[1]) / 2f; |
737 | 0 | float zCentre = (seqMin[2] + seqMax[2]) / 2f; |
738 | ||
739 | 0 | return new float[] { xCentre, yCentre, zCentre }; |
740 | } | |
741 | ||
742 | /** | |
743 | * Adds any sequences whose displayed points are within the given rectangle to | |
744 | * the viewport's current selection. Intended for key 's' after dragging to | |
745 | * select a region of the PCA. | |
746 | * | |
747 | * @param x1 | |
748 | * @param y1 | |
749 | * @param x2 | |
750 | * @param y2 | |
751 | */ | |
752 | 0 | protected void rectSelect(int x1, int y1, int x2, int y2) |
753 | { | |
754 | 0 | float[] centre = getCentre(); |
755 | ||
756 | 0 | for (int i = 0; i < npoint; i++) |
757 | { | |
758 | 0 | SequencePoint sp = sequencePoints.get(i); |
759 | 0 | int tmp1 = (int) (((sp.coord.x - centre[0]) * getScaleFactor()) |
760 | * (getWidth() / 3.15) + (getWidth() / 2.0)); | |
761 | 0 | float pre1 = ((sp.coord.x - centre[0]) * getScaleFactor()); |
762 | 0 | int tmp2 = (int) (((sp.coord.y - centre[1]) * getScaleFactor()) |
763 | * (getHeight() / 1.70) + (getHeight() / 2.0)); | |
764 | 0 | float pre2 = ((sp.coord.y - centre[1]) * getScaleFactor()); |
765 | ||
766 | 0 | if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2)) |
767 | { | |
768 | 0 | if (av != null) |
769 | { | |
770 | 0 | SequenceI sequence = sp.getSequence(); |
771 | 0 | if (av.getSelectionGroup() == null) |
772 | { | |
773 | 0 | SequenceGroup sg = new SequenceGroup(); |
774 | 0 | sg.setEndRes(av.getAlignment().getWidth() - 1); |
775 | 0 | av.setSelectionGroup(sg); |
776 | } | |
777 | 0 | if (!av.getSelectionGroup().getSequences(null).contains(sequence)) |
778 | { | |
779 | 0 | av.getSelectionGroup().addSequence(sequence, true); |
780 | } | |
781 | } | |
782 | } | |
783 | } | |
784 | } | |
785 | ||
786 | /** | |
787 | * Answers the first sequence found whose point on the display is within 2 | |
788 | * pixels of the given coordinates, or null if none is found | |
789 | * | |
790 | * @param x | |
791 | * @param y | |
792 | * | |
793 | * @return | |
794 | */ | |
795 | 0 | protected SequenceI findSequenceAtPoint(int x, int y) |
796 | { | |
797 | 0 | int halfwidth = getWidth() / 2; |
798 | 0 | int halfheight = getHeight() / 2; |
799 | ||
800 | 0 | int found = -1; |
801 | 0 | int pix = Math.min(getWidth(), getHeight()); |
802 | 0 | float xWidth = Math.abs(seqMax[0] - seqMin[0]); |
803 | 0 | float yWidth = Math.abs(seqMax[1] - seqMin[1]); |
804 | 0 | float maxWidth = Math.max(xWidth, yWidth); |
805 | 0 | float scaleBy = pix * getScaleFactor() / (2f * maxWidth); |
806 | ||
807 | 0 | float[] centre = getCentre(); |
808 | ||
809 | 0 | for (int i = 0; i < npoint; i++) |
810 | { | |
811 | 0 | SequencePoint sp = sequencePoints.get(i); |
812 | 0 | int px = (int) ((sp.coord.x - centre[0]) * scaleBy) + halfwidth; |
813 | 0 | int py = (int) ((sp.coord.y - centre[1]) * scaleBy) + halfheight; |
814 | ||
815 | 0 | if ((Math.abs(px - x) < NEARBY) && (Math.abs(py - y) < NEARBY)) |
816 | { | |
817 | 0 | found = i; |
818 | 0 | break; |
819 | } | |
820 | } | |
821 | ||
822 | 0 | if (found != -1) |
823 | { | |
824 | 0 | return sequencePoints.get(found).getSequence(); |
825 | } | |
826 | else | |
827 | { | |
828 | 0 | return null; |
829 | } | |
830 | } | |
831 | ||
832 | /** | |
833 | * Answers the panel the PCA is associated with (all panels for this alignment | |
834 | * if 'associate with all panels' is selected). | |
835 | * | |
836 | * @return | |
837 | */ | |
838 | 0 | AlignmentPanel[] getAssociatedPanels() |
839 | { | |
840 | 0 | if (isApplyToAllViews()) |
841 | { | |
842 | 0 | return PaintRefresher.getAssociatedPanels(av.getSequenceSetId()); |
843 | } | |
844 | else | |
845 | { | |
846 | 0 | return new AlignmentPanel[] { ap }; |
847 | } | |
848 | } | |
849 | ||
850 | 1 | public Color getBackgroundColour() |
851 | { | |
852 | 1 | return getBgColour(); |
853 | } | |
854 | ||
855 | /** | |
856 | * Zooms in or out in response to mouse wheel movement | |
857 | */ | |
858 | 0 | @Override |
859 | public void mouseWheelMoved(MouseWheelEvent e) | |
860 | { | |
861 | 0 | double wheelRotation = e.getPreciseWheelRotation(); |
862 | 0 | if (wheelRotation > 0) |
863 | { | |
864 | 0 | zoom(ZOOM_IN); |
865 | 0 | repaint(); |
866 | } | |
867 | 0 | else if (wheelRotation < 0) |
868 | { | |
869 | 0 | zoom(ZOOM_OUT); |
870 | 0 | repaint(); |
871 | } | |
872 | } | |
873 | ||
874 | /** | |
875 | * Answers the sequence point minimum [x, y, z] values. Note these are derived | |
876 | * when sequence points are set, but x and y values are not updated on | |
877 | * rotation (because this would result in changes to scaling). | |
878 | * | |
879 | * @return | |
880 | */ | |
881 | 1 | public float[] getSeqMin() |
882 | { | |
883 | 1 | return seqMin; |
884 | } | |
885 | ||
886 | /** | |
887 | * Answers the sequence point maximum [x, y, z] values. Note these are derived | |
888 | * when sequence points are set, but x and y values are not updated on | |
889 | * rotation (because this would result in changes to scaling). | |
890 | * | |
891 | * @return | |
892 | */ | |
893 | 1 | public float[] getSeqMax() |
894 | { | |
895 | 1 | return seqMax; |
896 | } | |
897 | ||
898 | /** | |
899 | * Sets the minimum and maximum [x, y, z] positions for sequence points. For | |
900 | * use when restoring a saved PCA from state data. | |
901 | * | |
902 | * @param min | |
903 | * @param max | |
904 | */ | |
905 | 1 | public void setSeqMinMax(float[] min, float[] max) |
906 | { | |
907 | 1 | seqMin = min; |
908 | 1 | seqMax = max; |
909 | } | |
910 | ||
911 | 1 | public float getScaleFactor() |
912 | { | |
913 | 1 | return scaleFactor; |
914 | } | |
915 | ||
916 | 3 | public void setScaleFactor(float scaleFactor) |
917 | { | |
918 | 3 | this.scaleFactor = scaleFactor; |
919 | } | |
920 | ||
921 | 1 | public boolean isShowLabels() |
922 | { | |
923 | 1 | return showLabels; |
924 | } | |
925 | ||
926 | 3 | public void setShowLabels(boolean showLabels) |
927 | { | |
928 | 3 | this.showLabels = showLabels; |
929 | } | |
930 | ||
931 | 1 | public boolean isApplyToAllViews() |
932 | { | |
933 | 1 | return applyToAllViews; |
934 | } | |
935 | ||
936 | 4 | public void setApplyToAllViews(boolean applyToAllViews) |
937 | { | |
938 | 4 | this.applyToAllViews = applyToAllViews; |
939 | } | |
940 | ||
941 | 10 | public Point[] getAxisEndPoints() |
942 | { | |
943 | 10 | return axisEndPoints; |
944 | } | |
945 | ||
946 | 4 | public void setAxisEndPoints(Point[] axisEndPoints) |
947 | { | |
948 | 4 | this.axisEndPoints = axisEndPoints; |
949 | } | |
950 | ||
951 | 1 | public Color getBgColour() |
952 | { | |
953 | 1 | return bgColour; |
954 | } | |
955 | ||
956 | 3 | public void setBgColour(Color bgColour) |
957 | { | |
958 | 3 | this.bgColour = bgColour; |
959 | } | |
960 | } |