/******************************************************************************* * 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; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Image; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelListener; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; import javax.swing.border.Border; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import edu.yu.einstein.genplay.core.manager.application.ConfigurationManager; import edu.yu.einstein.genplay.gui.event.genomeWindowEvent.GenomeWindowEvent; import edu.yu.einstein.genplay.gui.event.genomeWindowEvent.GenomeWindowListener; import edu.yu.einstein.genplay.gui.event.trackEvent.TrackEvent; import edu.yu.einstein.genplay.gui.event.trackEvent.TrackEventType; import edu.yu.einstein.genplay.gui.event.trackEvent.TrackEventsGenerator; import edu.yu.einstein.genplay.gui.event.trackEvent.TrackListener; import edu.yu.einstein.genplay.gui.track.layer.GeneLayer; import edu.yu.einstein.genplay.gui.track.layer.Layer; import edu.yu.einstein.genplay.gui.track.layer.background.BackgroundData; import edu.yu.einstein.genplay.gui.track.layer.background.BackgroundLayer; import edu.yu.einstein.genplay.gui.track.layer.foreground.ForegroundData; import edu.yu.einstein.genplay.gui.track.layer.foreground.ForegroundLayer; import edu.yu.einstein.genplay.util.Images; /** * Track component showing the data in GenPlay. * A track contains two subcomponents: the track handle and the graphics panel showing the data * @author Julien Lajugie */ public final class Track implements Serializable, GenomeWindowListener, TrackListener, TrackEventsGenerator, ListDataListener { private static final long serialVersionUID = 818958034840761257L; // generated ID private static final int SAVED_FORMAT_VERSION_NUMBER = 1; // saved format version private transient List<TrackListener> trackListeners; // list of track listeners private transient int defaultHeight; // default height of a track private transient JPanel trackPanel; // panel containing the track private transient HandlePanel handlePanel; // handle panel of the track private transient GraphicsPanel graphicsPanel; // graphics panel of the track private String trackName; // name of the track private int trackNumber; // number of the track private BackgroundLayer backgroundLayer; // background layer of the track (with the vertical and horizontal lines) private ForegroundLayer foregroundLayer; // foreground layer of the track (with the track name and the multi genome legend) private TrackModel layers; // layers of the track private Layer<?> activeLayer; // active layer of the track private TrackScore score; // score of the track /** * Creates an instance of {@link Track} * @param trackNumber number of the track */ public Track(int trackNumber) { super(); // create the panels trackPanel = new JPanel(); handlePanel = new HandlePanel(trackNumber); graphicsPanel = new GraphicsPanel(); // initializes the foreground and background drawer backgroundLayer = new BackgroundLayer(this); foregroundLayer = new ForegroundLayer(this); // set the track number this.trackNumber = trackNumber; // set the track score score = new TrackScore(this); // initializes the layer list layers = new TrackModel(); layers.addListDataListener(this); initTrack(); } @Override public void addTrackListener(TrackListener trackListener) { if (!trackListeners.contains(trackListener)) { trackListeners.add(trackListener); } } @Override public void contentsChanged(ListDataEvent e) { // case where the active layer was removed if (!layers.contains(activeLayer)) { setActiveLayer(null); } if ((activeLayer == null) && !layers.isEmpty()) { setActiveLayer(layers.getLayers()[0]); } updateGraphicsPanelDrawers(); getScore().autorescaleScoreAxis(); } /** * Draws an animation in the middle of the track * showing that the track is being loaded * @param g graphics where to draw the animation */ public void drawLoadingAnimation(Graphics g) { Image image = Images.getLoadingImage(); int x = (graphicsPanel.getWidth() - image.getWidth(null)) / 2; int y = (graphicsPanel.getHeight() - image.getHeight(null)) / 2; g.drawImage(image, x, y, trackPanel); } @Override public void genomeWindowChanged(GenomeWindowEvent evt) { // repaint the layers if the genome window changed graphicsPanel.repaint(); } /** * @return the active track layer */ public Layer<?> getActiveLayer() { return activeLayer; } /** * @return the background layer of the track */ public Layer<BackgroundData> getBackgroundLayer() { return backgroundLayer; } /** * @return the default height of the track */ public int getDefaultHeight() { return defaultHeight; } /** * @return the default track name */ private String getDefaultName() { return new String(TrackConstants.NAME_PREFIX + getNumber()); } /** * @param font * @return the font metrics of the specified font */ public FontMetrics getFontMetrics() { return trackPanel.getGraphics().getFontMetrics(); } /** * @param font * @return the font metrics of the specified font */ public FontMetrics getFontMetrics(Font font) { return trackPanel.getFontMetrics(font); } /** * @return the foreground layer of the track */ public Layer<ForegroundData> getForegroundLayer() { return foregroundLayer; } /** * @return the panel where the layers are drawn */ public GraphicsPanel getGraphicsPanel() { return graphicsPanel; } /** * @return the height of the track panel. Shortcut for getTrackPanel().getHeight() */ public int getHeight() { return trackPanel.getHeight(); } /** * @return an image of the track (without its handle) */ public BufferedImage getImage() { BufferedImage image = new BufferedImage(graphicsPanel.getWidth(), graphicsPanel.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics g = image.createGraphics(); graphicsPanel.paint(g); return image; } /** * @return the {@link TrackModel} managing the list of layers of the track */ public TrackModel getLayers() { return layers; } /** * @return the name of the track */ public String getName() { if (trackName == null) { // default track name if it hasn't been set return getDefaultName(); } else { return trackName; } } /** * @return the number of the track */ public int getNumber() { return trackNumber; } /** * @return the object that manages the score of the track */ public TrackScore getScore() { return score; } @Override public TrackListener[] getTrackListeners() { TrackListener[] listeners = new TrackListener[trackListeners.size()]; return trackListeners.toArray(listeners); } /** * @return the top level panel of the track */ public JPanel getTrackPanel() { return trackPanel; } /** * Initialize the track * @param trackNumber number of the track */ private void initTrack() { // set the minimum and maximum height of the track trackPanel.setMinimumSize(new Dimension(trackPanel.getMinimumSize().width, TrackConstants.MINIMUM_HEIGHT)); trackPanel.setMaximumSize(new Dimension(trackPanel.getMaximumSize().width, TrackConstants.MAXIMUM_HEIGHT)); // add the panels BorderLayout layout = new BorderLayout(); trackPanel.setLayout(layout); trackPanel.add(handlePanel, BorderLayout.LINE_START); trackPanel.add(graphicsPanel, BorderLayout.CENTER); // sets the track number setNumber(trackNumber); // we update the list of drawers registered to the graphics panel updateGraphicsPanelDrawers(); // register itself to the handle so the track can be notified when there is an action on the handle handlePanel.addTrackListener(this); // set the the default height of the track ConfigurationManager configurationManager = ConfigurationManager.getInstance(); int defaultHeight = configurationManager.getTrackHeight(); setDefaultHeight(defaultHeight); setPreferredHeight(defaultHeight); // set the border of the track trackPanel.setBorder(TrackConstants.REGULAR_BORDER); // create list of track listener trackListeners = new ArrayList<TrackListener>(); } @Override public void intervalAdded(ListDataEvent e) { updateGraphicsPanelDrawers(); getScore().autorescaleScoreAxis(); } @Override public void intervalRemoved(ListDataEvent e) { // case where the active layer was removed if (!layers.contains(activeLayer)) { setActiveLayer(null); } updateGraphicsPanelDrawers(); getScore().autorescaleScoreAxis(); } /** * @return true if the track is selected, false otherwise */ public boolean isSelected() { // a track is selected if its handle is selected return handlePanel.isSelected(); } /** * Locks the track handle */ public void lockHandle() { handlePanel.lock(); } /** * Notifies all the track listeners that the track has changed * @param trackEventType track event type */ public void notifyTrackListeners(TrackEventType trackEventType) { if (trackListeners != null) { TrackEvent trackEvent = new TrackEvent(this, trackEventType); for (TrackListener listener: trackListeners) { listener.trackChanged(trackEvent); } } } /** * Method used for unserialization * @param in * @throws IOException * @throws ClassNotFoundException */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { int savedVersion = in.readInt(); trackNumber = in.readInt(); // recreate panels trackPanel = new JPanel(); handlePanel = new HandlePanel(trackNumber); graphicsPanel = new GraphicsPanel(); // set the background and foreground layers backgroundLayer = (BackgroundLayer) in.readObject(); backgroundLayer.setTrack(this); foregroundLayer = (ForegroundLayer) in.readObject(); foregroundLayer.setTrack(this); // initialize the track // add the tracks int layerCount = in.readInt(); if (layerCount > 0) { layers = (TrackModel) in.readObject(); // since the track field of layers is transient we have to set it for (Layer<?> currentLayer: layers) { currentLayer.setTrack(this); } setActiveLayer((Layer<?>) in.readObject()); } else { layers = new TrackModel(); layers.addListDataListener(this); } score = (TrackScore) in.readObject(); // initialize the track initTrack(); // restore the height of the track setPreferredHeight(in.readInt()); if (savedVersion >= 1) { trackName = (String) in.readObject(); } } @Override public void removeTrackListener(TrackListener trackListener) { trackListeners.remove(trackListener); } /** * Repaints the track panel and all its components */ public void repaint() { trackPanel.repaint(); } /** * Sets the active layer of the track if the specified layer is one of the layers of the track. * Sets the active layer to null if the specified parameter is null. * Does nothing if the specified layer is not null and is not one of the layers of the track * @param activeLayer the active layer to set */ public void setActiveLayer(Layer<?> activeLayer) { Layer<?> oldLayer = getActiveLayer(); if (activeLayer == null) { this.activeLayer = null; } else if (layers.contains(activeLayer)) { this.activeLayer = activeLayer; } // update the mouse listeners of the graphics panel if (oldLayer != activeLayer) { if (oldLayer != null) { if (oldLayer instanceof MouseMotionListener) { graphicsPanel.removeMouseMotionListener((MouseMotionListener)oldLayer); } if (oldLayer instanceof MouseListener) { graphicsPanel.removeMouseListener((MouseListener)oldLayer); } if (oldLayer instanceof MouseWheelListener) { graphicsPanel.removeMouseWheelListener((MouseWheelListener)oldLayer); } } if (activeLayer != null) { if (activeLayer instanceof MouseMotionListener) { graphicsPanel.addMouseMotionListener((MouseMotionListener)activeLayer); } if (activeLayer instanceof MouseListener) { graphicsPanel.addMouseListener((MouseListener)activeLayer); } if (activeLayer instanceof MouseWheelListener) { graphicsPanel.addMouseWheelListener((MouseWheelListener)activeLayer); } } } } /** * Sets the border of the track. * @param border */ public void setBorder(Border border) { getTrackPanel().setBorder(border); } /** * Sets the current track with the parameters and the layers of the specified track * @param otherTrack */ public void setContentAs(Track otherTrack) { foregroundLayer = otherTrack.foregroundLayer.clone(); foregroundLayer.setTrack(this); backgroundLayer = otherTrack.backgroundLayer.clone(); backgroundLayer.setTrack(this); for (Layer<?> currentLayer: otherTrack.layers) { Layer<?> layerToAdd = currentLayer.clone(); layerToAdd.setTrack(this); getLayers().add(layerToAdd); } if ((otherTrack.trackName != null) && !otherTrack.trackName.equals(otherTrack.getDefaultName())) { trackName = otherTrack.trackName; } score.setMinimumScore(otherTrack.score.getMinimumScore()); score.setMaximumScore(otherTrack.score.getMaximumScore()); score.setScoreAxisAutorescaled(otherTrack.score.isScoreAxisAutorescaled()); } /** * Sets the default height of the track * @param defaultHeight default height to set */ public void setDefaultHeight(int defaultHeight) { this.defaultHeight = defaultHeight; } /** * Makes the handle of the track visible or invisible * @param isVisible true to make the handle visible; false to make it invisible */ public void setHandleVisible(boolean isVisible) { handlePanel.setVisible(isVisible); } /** * Sets the name of the track * @param name */ public void setName(String name) { if ((name != null) && !name.equals(getDefaultName())) { trackName = name; } } /** * Sets the number of the track * @param number number to set */ public void setNumber(int number) { trackNumber = number; handlePanel.setNumber(number); } /** * Sets the height of the track * @param preferredHeight preferred height to set */ public void setPreferredHeight(int preferredHeight) { // update the dimension of the track panel Dimension trackDimension = new Dimension(trackPanel.getPreferredSize().width, preferredHeight); trackPanel.setPreferredSize(trackDimension); trackPanel.revalidate(); } /** * Sets if the track is selected or not * @param isSelected true if the track is selected, false otherwise */ public void setSelected(boolean isSelected) { // a track is selected if its handle is selected handlePanel.setSelected(isSelected); } @Override public String toString() { if (getName() != null) { return getName(); } else { return super.toString(); } } @Override public void trackChanged(TrackEvent evt) { if (evt.getEventType() == TrackEventType.RESIZED) { // resize event setPreferredHeight(handlePanel.getNewHeight()); } else if (evt.getEventType() == TrackEventType.SIZE_SET_TO_DEFAULT) { // size set to default event setPreferredHeight(getDefaultHeight()); } else { // other event // we relay the other events to the element that contains this track notifyTrackListeners(evt.getEventType()); } } /** * Unlocks the track handle */ public void unlockHandle() { handlePanel.unlock(); } /** * Updates the cursor displayed on the graphic panel */ public void updateGraphicCursor() { if (foregroundLayer.isCursorOverLegend()) { graphicsPanel.setCursor(TrackConstants.CURSOR_OVER_LEGEND); } else if ((activeLayer instanceof GeneLayer) && (((GeneLayer) activeLayer).getGeneUnderMouse() != null)) { graphicsPanel.setCursor(TrackConstants.CURSOR_OVER_GENE); } else { graphicsPanel.setCursor(TrackConstants.DEFAULT_CURSOR); } } /** * Updates the list of drawers registered to the graphics panel. * There are one drawer per layer including the background and foreground layers * that need to be registered. */ private void updateGraphicsPanelDrawers() { if (layers == null) { // case where there is no other layer than the foreground and the background Drawer[] drawers = {backgroundLayer, foregroundLayer}; graphicsPanel.setDrawers(drawers); } else { // case where there are other layers Drawer[] drawers = new Drawer[layers.size() + 2]; drawers[0] = backgroundLayer; int i = 1; // we want to draw the layers in reverse order because the first layers // of the list should be on top for (int j = layers.size() - 1; j >= 0; j--) { Drawer currentDrawer = layers.getLayers()[j]; drawers[i] = currentDrawer; i++; } drawers[i] = foregroundLayer; graphicsPanel.setDrawers(drawers); } } /** * Method used for serialization * @param out * @throws IOException */ private void writeObject(ObjectOutputStream out) throws IOException { out.writeInt(SAVED_FORMAT_VERSION_NUMBER); out.writeInt(trackNumber); out.writeObject(backgroundLayer); out.writeObject(foregroundLayer); // write the number of layers if ((layers == null) || layers.isEmpty()) { out.writeInt(0); } else { out.writeInt(layers.size()); out.writeObject(layers); // make sure the active layer is not null if (activeLayer == null) { setActiveLayer(layers.getLayers()[0]); } out.writeObject(activeLayer); } out.writeObject(score); // save the height of the track out.writeInt(trackPanel.getHeight()); if (SAVED_FORMAT_VERSION_NUMBER >= 1) { out.writeObject(getName()); } } }