/*******************************************************************************
* GenPlay, Einstein Genome Analyzer
* Copyright (C) 2009, 2014 Albert Einstein College of Medicine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* Authors: Julien Lajugie <julien.lajugie@einstein.yu.edu>
* Nicolas Fourel <nicolas.fourel@einstein.yu.edu>
* Eric Bouhassira <eric.bouhassira@einstein.yu.edu>
*
* Website: <http://genplay.einstein.yu.edu>
******************************************************************************/
package edu.yu.einstein.genplay.gui.track.layer;
import java.awt.Desktop;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import edu.yu.einstein.genplay.core.manager.project.ProjectManager;
import edu.yu.einstein.genplay.core.manager.project.ProjectWindow;
import edu.yu.einstein.genplay.dataStructure.gene.Gene;
import edu.yu.einstein.genplay.dataStructure.list.genomeWideList.geneList.GeneList;
import edu.yu.einstein.genplay.dataStructure.list.listView.ListView;
import edu.yu.einstein.genplay.dataStructure.scoredChromosomeWindow.ScoredChromosomeWindow;
import edu.yu.einstein.genplay.exception.ExceptionManager;
import edu.yu.einstein.genplay.gui.dataScalerForTrackDisplay.DataScalerManager;
import edu.yu.einstein.genplay.gui.track.ScrollingManager;
import edu.yu.einstein.genplay.gui.track.Track;
import edu.yu.einstein.genplay.util.NumberFormats;
import edu.yu.einstein.genplay.util.colors.Colors;
/**
* Layer displaying a {@link GeneList}
* @author Julien Lajugie
*/
public class GeneLayer extends AbstractVersionedLayer<GeneList> implements Layer<GeneList>, VersionedLayer<GeneList>, ScoredLayer, MouseListener, MouseMotionListener, MouseWheelListener {
/** The name of the genes are printed if the ratio is higher than this value */
public static final double MIN_X_RATIO_PRINT_NAME = 0.0005d;
private static final long serialVersionUID = 3779631846077486596L; // generated ID
private static final int SAVED_FORMAT_VERSION_NUMBER = 0; // Saved format version
private static final float SCORE_SATURATION = 0.02f; // saturation of the score of the exon for the display
private static final short GENE_HEIGHT = 6; // size of a gene in pixel
private static final short UTR_HEIGHT = 3; // height of a UTR region of a gene in pixel
private int firstLineToDisplay; // number of the first line to be displayed
private int geneLinesCount; // number of line of genes
private int mouseStartDragY = -1; // position of the mouse when start dragging
private Gene geneUnderMouse = null; // gene under the cursor of the mouse
private float min; // minimum score of the GeneList to display
private float max; // maximum score of the GeneList to display
/**
* Creates an instance of {@link GeneLayer} with the same properties as the specified {@link GeneLayer}.
* The copy of the data is shallow.
* @param binLayer
*/
private GeneLayer(GeneLayer geneLayer) {
super(geneLayer);
firstLineToDisplay = geneLayer.firstLineToDisplay;
geneLinesCount = geneLayer.geneLinesCount;
mouseStartDragY = -1;
geneUnderMouse = null;
min = geneLayer.min;
max = geneLayer.max;
}
/**
* Creates an instance of a {@link GeneLayer}
* @param track track containing the layer
* @param data data of the layer
* @param name name of the layer
*/
public GeneLayer(Track track, GeneList data, String name) {
super(track, data, name);
firstLineToDisplay = 0;
geneLinesCount = 0;
mouseStartDragY = -1;
geneUnderMouse = null;
setSaturatedMinMax();
}
/**
* {@inheritDoc}
*/
@Override
public GeneLayer clone() {
return new GeneLayer(this);
}
/**
* Draws the genes
* @param g {@link Graphics}
*/
@Override
public void draw(Graphics g, int width, int height) {
if (isVisible()) {
// we retrieve the project window
ProjectWindow projectWindow = ProjectManager.getInstance().getProjectWindow();
// we retrieve the minimum and maximum scores displayed in the track
float max = getTrack().getScore().getMaximumScore();
float min = getTrack().getScore().getMinimumScore();
// we print the gene names if the x ratio > MIN_X_RATIO_PRINT_NAME
boolean isGeneNamePrinted = projectWindow.getXRatio() > MIN_X_RATIO_PRINT_NAME;
// Retrieve the genes to print
List<ListView<Gene>> genesToPrint = DataScalerManager.getInstance().getScaledData(this);
if (genesToPrint == null) {
getTrack().drawLoadingAnimation(g);
} else {
// Compute the maximum number of line displayable
int displayedLineCount = 0;
if (isGeneNamePrinted) {
displayedLineCount = ((height - (2 * GENE_HEIGHT)) / (GENE_HEIGHT * 3)) + 1;
} else {
displayedLineCount = ((height - GENE_HEIGHT) / (GENE_HEIGHT * 2)) + 1;
}
// calculate how many scroll on the Y axis are necessary to show all the genes
geneLinesCount = (genesToPrint.size() - displayedLineCount) + 2;
// For each line of genes on the screen
for (int i = 0; i < displayedLineCount; i++) {
// Calculate the height of the gene
int currentHeight;
if (isGeneNamePrinted) {
currentHeight = (i * (GENE_HEIGHT * 3)) + (2 * GENE_HEIGHT);
} else {
currentHeight = (i * (GENE_HEIGHT * 2)) + GENE_HEIGHT;
}
// Calculate which line has to be printed depending on the position of the scroll bar
int currentLine = i + firstLineToDisplay;
if (currentLine < genesToPrint.size()) {
// For each gene of the current line
if (genesToPrint.get(currentLine) != null) {
for (Gene geneToPrint : genesToPrint.get(currentLine)) {
// retrieve the screen x coordinate of the start and stop position
int x1 = projectWindow.genomeToScreenPosition(geneToPrint.getStart());
int x2 = projectWindow.genomeToScreenPosition(geneToPrint.getStop());
if (x2 != 0) {
// Choose the color depending on if the gene is under the mouse and on the strand
boolean isHighlighted = ((geneUnderMouse != null) && (geneToPrint.equals(geneUnderMouse)));
g.setColor(Colors.geneToColor(geneToPrint.getStrand(), isHighlighted));
// Draw the gene
g.drawLine(x1, currentHeight, x2, currentHeight);
// Draw the name of the gene if the zoom is small enough
if (isGeneNamePrinted) {
String geneName = geneToPrint.getName();
if (geneToPrint.getStart() < projectWindow.getGenomeWindow().getStart()) {
int newX = (int)Math.round((geneToPrint.getStart() - projectWindow.getGenomeWindow().getStart()) * projectWindow.getXRatio()); // former method
g.drawString(geneName, newX, currentHeight - 1);
} else {
g.drawString(geneName, x1, currentHeight - 1);
}
}
// For each exon of the current gene
if (geneToPrint.getExons() != null) {
for (int j = 0; j < geneToPrint.getExons().size(); j++) {
ScoredChromosomeWindow currentExon = geneToPrint.getExons().get(j);
int exonX = projectWindow.genomeToScreenPosition(currentExon.getStart());
if (currentExon.getStop() >= projectWindow.getGenomeWindow().getStart()) {
int exonWidth = projectWindow.genomeToScreenPosition(currentExon.getStop()) - exonX;
if (exonWidth < 1) {
exonWidth = 1;
}
// if we have some exon score values
if (!Float.isNaN(currentExon.getScore())) {
g.setColor(Colors.scoreToColor(currentExon.getScore(), min, max));
} else {
g.setColor(Colors.geneToColor(geneToPrint.getStrand(), isHighlighted));
}
// case where the exon is not at all in a UTR (untranslated region)
if ((currentExon.getStart() >= geneToPrint.getUTR5Bound()) && (currentExon.getStop() <= geneToPrint.getUTR3Bound())) {
g.fillRect(exonX, currentHeight + 1, exonWidth, GENE_HEIGHT);
} else {
// case where the whole exon is in a UTR
if ((currentExon.getStop() <= geneToPrint.getUTR5Bound()) || (currentExon.getStart() >= geneToPrint.getUTR3Bound())) {
g.fillRect(exonX, currentHeight + 1, exonWidth, UTR_HEIGHT);
} else {
// case where the exon is in both UTR
if ((currentExon.getStart() <= geneToPrint.getUTR5Bound()) && (currentExon.getStop() >= geneToPrint.getUTR3Bound())) {
int UTR5Width = projectWindow.genomeToScreenPosition(geneToPrint.getUTR5Bound()) - exonX;
int TRWidth = projectWindow.genomeToScreenPosition(geneToPrint.getUTR3Bound()) - exonX - UTR5Width;
int UTR3Width = exonWidth - UTR5Width - TRWidth;
g.fillRect(exonX, currentHeight + 1, UTR5Width, UTR_HEIGHT);
g.fillRect(exonX + UTR5Width, currentHeight + 1, TRWidth, GENE_HEIGHT);
g.fillRect(exonX + UTR5Width + TRWidth, currentHeight + 1, UTR3Width, UTR_HEIGHT);
} else {
// case where part of the exon is in the UTR and part is not
if ((currentExon.getStart() <= geneToPrint.getUTR5Bound()) && (currentExon.getStop() >= geneToPrint.getUTR5Bound())) {
// case where part is in the 5'UTR
int UTRWidth = projectWindow.genomeToScreenPosition(geneToPrint.getUTR5Bound()) - exonX;
g.fillRect(exonX, currentHeight + 1, UTRWidth, UTR_HEIGHT);
g.fillRect(exonX + UTRWidth, currentHeight + 1, exonWidth - UTRWidth, GENE_HEIGHT);
} else if ((currentExon.getStart() <= geneToPrint.getUTR3Bound()) && (currentExon.getStop() >= geneToPrint.getUTR3Bound())) {
// case where part is in the 3' UTR
int TRWidth = projectWindow.genomeToScreenPosition(geneToPrint.getUTR3Bound()) - exonX; // TRWidth is the with of the TRANSLATED region
g.fillRect(exonX, currentHeight + 1, TRWidth, GENE_HEIGHT);
g.fillRect(exonX + TRWidth, currentHeight + 1, exonWidth - TRWidth, UTR_HEIGHT);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
@Override
public float getCurrentScoreToDisplay() {
// we return null because they could be more than one score to display at a position
return Float.NaN;
}
/**
* @return the gene under the mouse. Null if none
*/
public Gene getGeneUnderMouse() {
return geneUnderMouse;
}
@Override
public float getMaximumScoreToDisplay() {
return max;
}
@Override
public float getMinimumScoreToDisplay() {
return min;
}
@Override
public LayerType getType() {
return LayerType.GENE_LAYER;
}
@Override
public void mouseClicked(MouseEvent e) {
if (isVisible()) {
// if a gene is double clicked
if ((e.getClickCount() == 2) && (geneUnderMouse != null)) {
// if the desktop is supported
if ((getData().getGeneDBURL() != null) && (Desktop.isDesktopSupported())) {
try {
// we open a browser showing information on the gene
Desktop.getDesktop().browse(new URI(getData().getGeneDBURL() + geneUnderMouse.getName()));
} catch (Exception e1) {
ExceptionManager.getInstance().caughtException(Thread.currentThread(), e1, "Error while opening the web browser");
}
}
}
}
}
/**
* Changes the scroll position of the panel when mouse dragged with the right button
*/
@Override
public void mouseDragged(MouseEvent e) {
if (isVisible()) {
// we retrieve the project window
ProjectWindow projectWindow = ProjectManager.getInstance().getProjectWindow();
// we print the gene names if the x ratio > MIN_X_RATIO_PRINT_NAME
boolean isGeneNamePrinted = projectWindow.getXRatio() > MIN_X_RATIO_PRINT_NAME;
if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
int distance = 0;
if (isGeneNamePrinted) {
distance = (mouseStartDragY - e.getY()) / (3 * GENE_HEIGHT);
} else {
distance = (mouseStartDragY - e.getY()) / (2 * GENE_HEIGHT);
}
if (Math.abs(distance) > 0) {
if (((distance < 0) && ((distance + firstLineToDisplay) >= 0))
|| ((distance > 0) && ((distance + firstLineToDisplay) <= geneLinesCount))) {
firstLineToDisplay += distance;
mouseStartDragY = e.getY();
getTrack().repaint();
}
}
}
}
}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
/**
* Retrieves the gene under the cursor of the mouse if there is one
*/
@Override
public void mouseMoved(MouseEvent e) {
if (isVisible()) {
if (!ScrollingManager.getInstance().isScrollingEnabled()) {
// we retrieve the project window
ProjectWindow projectWindow = ProjectManager.getInstance().getProjectWindow();
Gene oldGeneUnderMouse = geneUnderMouse;
geneUnderMouse = null;
// retrieve the position of the mouse
Point mousePosition = e.getPoint();
// check if the name of genes is printed
boolean isGeneNamePrinted = projectWindow.getXRatio() > MIN_X_RATIO_PRINT_NAME;
// retrieve the list of the printed genes
List<ListView<Gene>> printedGenes = DataScalerManager.getInstance().getScaledData(this);
// do nothing if there is no genes
if (printedGenes == null) {
return;
}
// look for how many lines of genes are printed
int displayedLineCount = printedGenes.size();
// search if the mouse is on a line where there is genes printed on the track
int mouseLine = -1;
int i = 0;
while ((mouseLine == -1) && (i < displayedLineCount)) {
if (isGeneNamePrinted) {
if ((mousePosition.y >= ((i * GENE_HEIGHT * 3) + GENE_HEIGHT)) &&
(mousePosition.y <= ((i * GENE_HEIGHT * 3) + (3 * GENE_HEIGHT)))) {
mouseLine = i;
}
} else {
if ((mousePosition.y >= ((i * GENE_HEIGHT * 2) + GENE_HEIGHT)) &&
(mousePosition.y <= ((i * GENE_HEIGHT * 2) + (2 * GENE_HEIGHT)))) {
mouseLine = i;
}
}
i++;
}
// if we found something
if (mouseLine != -1) {
// line of genes where the mouse is
mouseLine += firstLineToDisplay;
if (mouseLine < printedGenes.size()) {
// search if the x position of the mouse is on a gene too
int j = 0;
while ((j < printedGenes.get(mouseLine).size()) && (geneUnderMouse == null)) {
Gene currentGene = printedGenes.get(mouseLine).get(j);
if ((mousePosition.x >= projectWindow.genomeToScreenPosition(currentGene.getStart())) &&
(mousePosition.x <= projectWindow.genomeToScreenPosition(currentGene.getStop()))) {
// we found a gene under the mouse
geneUnderMouse = currentGene;
}
j++;
}
}
}
// unset the tool text and the mouse cursor if there is no gene under the mouse
if (geneUnderMouse == null) {
getTrack().getGraphicsPanel().setToolTipText(null);
getTrack().updateGraphicCursor();
} else {
// if there is a gene under the mouse we also check
// if there is an exon with a score under the mouse cursor
float scoreExonUnderMouse = Float.NaN;
if ((geneUnderMouse.getExons() != null) && (geneUnderMouse.getExons().size() > 0)) {
for (int k = 0; (k < geneUnderMouse.getExons().size()) && (Float.isNaN(scoreExonUnderMouse)); k++) {
ScoredChromosomeWindow currentExon = geneUnderMouse.getExons().get(k);
if ((mousePosition.x >= projectWindow.genomeToScreenPosition(currentExon.getStart())) &&
(mousePosition.x <= projectWindow.genomeToScreenPosition(currentExon.getStop()))) {
scoreExonUnderMouse = currentExon.getScore();
}
}
}
// set the cursor and the tooltip text if there is a gene under the mouse cursor
getTrack().updateGraphicCursor();
String toolTipText = "<html><b>" + geneUnderMouse.getName() + "</b><br>";
GeneList geneList = getData();
if (geneList.getGeneScoreType() != null) {
toolTipText += "Score Type: <i>" + geneList.getGeneScoreType() + "</i><br>";
}
if (!Float.isNaN(geneUnderMouse.getScore())) {
toolTipText += "Gene Score = <i>" + NumberFormats.getScoreFormat().format(geneUnderMouse.getScore()) + "</i><br>";
}
if (!Float.isNaN(scoreExonUnderMouse)) {
toolTipText += "Exon Score = <i>" + NumberFormats.getScoreFormat().format(scoreExonUnderMouse) + "</i><br>";
}
toolTipText += "</html>";
getTrack().getGraphicsPanel().setToolTipText(toolTipText);
}
// we repaint the track only if the gene under the mouse changed
if (((oldGeneUnderMouse == null) && (geneUnderMouse != null))
|| ((oldGeneUnderMouse != null) && (!oldGeneUnderMouse.equals(geneUnderMouse)))) {
getTrack().repaint();
}
}
}
}
/**
* Sets the variable mouseStartDragY when the user press the right button of the mouse
*/
@Override
public void mousePressed(MouseEvent e) {
if (isVisible()) {
if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
mouseStartDragY = e.getY();
}
}
}
@Override
public void mouseReleased(MouseEvent e) {}
/**
* Changes the scroll position of the panel when the wheel of the mouse is used with the right button
*/
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (isVisible()) {
if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
if (((e.getWheelRotation() < 0) && ((e.getWheelRotation() + firstLineToDisplay) >= 0))
|| ((e.getWheelRotation() > 0) && ((e.getWheelRotation() + firstLineToDisplay) <= geneLinesCount))) {
firstLineToDisplay += e.getWheelRotation();
getTrack().repaint();
}
}
}
}
/**
* Method used for deserialization
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.readInt();
min = in.readFloat();
max = in.readFloat();
firstLineToDisplay = 0;
geneLinesCount = 0;
mouseStartDragY = -1;
geneUnderMouse = null;
}
@Override
public void setData(GeneList data) {
super.setData(data);
}
@Override
public void setData(GeneList data, String description) {
super.setData(data, description);
}
/**
* Computes the minimum and maximum saturated values of the exon scores
*/
private void setSaturatedMinMax() {
// put the scores of every exon in a big list
List<Float> scoreList = new ArrayList<Float>();
for (ListView<Gene> currentList: getData()) {
if ((currentList != null) && (!currentList.isEmpty())) {
for (Gene currentGene: currentList) {
if (currentGene.getExons() != null) {
for (ScoredChromosomeWindow currentExon: currentGene.getExons()) {
if ((!Float.isNaN(currentExon.getScore())) && (currentExon.getScore() != 0)) {
scoreList.add(currentExon.getScore());
}
}
}
}
}
}
if (!scoreList.isEmpty()) {
// sort the list
Collections.sort(scoreList);
int minIndex = (int)(SCORE_SATURATION * (scoreList.size() - 1));
int maxIndex = (scoreList.size() - 1) - (int)((SCORE_SATURATION * scoreList.size()) - 1);
min = scoreList.get(minIndex);
max = scoreList.get(maxIndex);
}
}
/**
* Method used for serialization
* @param out
* @throws IOException
*/
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(SAVED_FORMAT_VERSION_NUMBER);
out.writeFloat(min);
out.writeFloat(max);
}
}