/*
 * Copyright (c) 2007-2012 The Broad Institute, Inc.
 * SOFTWARE COPYRIGHT NOTICE
 * This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
 *
 * This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
 *
 * This software is licensed under the terms of the GNU Lesser General Public License (LGPL),
 * Version 2.1 which is available at http://www.opensource.org/licenses/lgpl-2.1.php.
 */
package org.broad.igv.track;

//~--- non-JDK imports --------------------------------------------------------

import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.PreferenceManager;
import org.broad.igv.gwas.GWASTrack;
import org.broad.igv.renderer.*;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.sam.CoverageTrack;
import org.broad.igv.session.IGVSessionReader;
import org.broad.igv.session.SessionXmlAdapters;
import org.broad.igv.session.SubtlyImportant;
import org.broad.igv.ui.FontManager;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.TooltipTextFrame;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.panel.AttributeHeaderPanel;
import org.broad.igv.ui.panel.IGVPopupMenu;
import org.broad.igv.ui.panel.MouseableRegion;
import org.broad.igv.ui.panel.ReferenceFrame;
import org.broad.igv.ui.util.UIUtilities;
import org.broad.igv.util.ResourceLocator;
import org.broad.igv.util.Utilities;
import org.broad.tribble.Feature;
import org.w3c.dom.Node;

import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;

/**
 * @author jrobinso
 */
@XmlType(factoryClass = IGVSessionReader.class, factoryMethod = "getNextTrack")
@XmlAccessorType(XmlAccessType.NONE)
@XmlSeeAlso({CoverageTrack.class, AlignmentTrack.class, DataSourceTrack.class, GWASTrack.class, FeatureTrack.class, MergedTracks.class})
public abstract class AbstractTrack implements Track {

    private static Logger log = Logger.getLogger(AbstractTrack.class);

    /**
     * Classes which we have tried to marshal/unmarshal
     * and have failed. Since we just use exception catching (slow),
     * we don't want to repeat failures
     */
    public static final Set<Class> knownUnknownTrackClasses = new HashSet<Class>();
    public static final Class defaultTrackClass = AbstractTrack.class;

    /**
     * Set default renderer classes by track type.
     */
    private static Class defaultRendererClass = BarChartRenderer.class;
    private static Map<TrackType, Class> defaultRendererMap = new HashMap();

    static {
        defaultRendererMap.put(TrackType.RNAI, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.COPY_NUMBER, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.CNV, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.ALLELE_SPECIFIC_COPY_NUMBER, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.GENE_EXPRESSION, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.DNA_METHYLATION, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.LOH, HeatmapRenderer.class);
        defaultRendererMap.put(TrackType.OTHER, BarChartRenderer.class);
        defaultRendererMap.put(TrackType.CHIP_CHIP, HeatmapRenderer.class);
    }


    @XmlAttribute
    private String id;
    @XmlAttribute
    private String name;
    private String url;
    private boolean itemRGB = true;

    private boolean useScore;
    private float viewLimitMin = Float.NaN;     // From UCSC track line
    private float viewLimitMax = Float.NaN;  // From UCSC track line


    @XmlAttribute
    protected int fontSize = PreferenceManager.getInstance().getAsInt(PreferenceManager.DEFAULT_FONT_SIZE);
    private boolean showDataRange = true;
    private String sampleId;

    private ResourceLocator resourceLocator;

    private int top;
    protected int minimumHeight = -1;
    protected int maximumHeight = 1000;

    private TrackType trackType = TrackType.OTHER;

    private boolean selected = false;

    @XmlAttribute
    private boolean visible = true;
    @XmlAttribute
    private boolean sortable = true;

    boolean overlaid;

    boolean drawYLine = false;
    float yLine = 0;

    // Map to store attributes specific to this track.  Attributes shared by multiple
    private Map<String, String> attributes = new HashMap();

    // Scale for heatmaps
    @XmlJavaTypeAdapter(SessionXmlAdapters.ContinuousColorScale.class)
    @XmlAttribute
    private ContinuousColorScale colorScale;

    //TODO Only write it out for applicable tracks
    //Not applicable to all tracks.
    @XmlAttribute
    protected boolean autoScale;

    @XmlJavaTypeAdapter(SessionXmlAdapters.Color.class)
    @XmlAttribute(name = "color")
    private Color posColor = Color.blue.darker();

    @XmlJavaTypeAdapter(SessionXmlAdapters.Color.class)
    @XmlAttribute
    private Color altColor = posColor;

    @XmlAttribute(name = "featureVisibilityWindow")
    protected int visibilityWindow = -1;
    @XmlAttribute
    private DisplayMode displayMode = DisplayMode.COLLAPSED;

    @XmlJavaTypeAdapter(SessionXmlAdapters.Height.class)
    @XmlAttribute
    protected Integer height = -1;

    @XmlElement(name = "DataRange")
    protected DataRange dataRange;

    @SubtlyImportant
    private AbstractTrack() {
    }

    public AbstractTrack(
            ResourceLocator dataResourceLocator,
            String id,
            String name) {
        this.resourceLocator = dataResourceLocator;
        this.id = id;
        this.name = name;
        init();
    }

    public AbstractTrack(ResourceLocator dataResourceLocator, String id) {
        this(dataResourceLocator, id, dataResourceLocator.getTrackName());
    }

    public AbstractTrack(ResourceLocator locator) {
        this(locator, locator != null ? locator.getPath() : null, locator != null ? locator.getTrackName() : null);
    }

    public AbstractTrack(String id) {
        this(null, id, id);
    }


    public AbstractTrack(String id, String name) {
        this(null, id, name);
    }

    private void init() {
        showDataRange = PreferenceManager.getInstance().getAsBoolean(PreferenceManager.CHART_SHOW_DATA_RANGE);
        if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.EXPAND_FEAUTRE_TRACKS)) {
            displayMode = DisplayMode.EXPANDED;
        }
    }

    public void setRendererClass(Class rc) {
        // Ignore by default
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setUseScore(boolean useScore) {
        this.useScore = useScore;
    }

    public String getId() {
        return id;
    }


    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    private String getDisplayName() {

        String sampleKey = IGV.getInstance().getSession().getTrackAttributeName();
        if (sampleKey != null && sampleKey.trim().length() > 0) {
            String name = getAttributeValue(sampleKey.trim());
            if (name != null) {
                return name;
            }
        }
        return getName();
    }


    public void setSampleId(String sampleId) {
        this.sampleId = sampleId;
    }

    @Override
    public void load(RenderContext context) {
        // No-op, to be overriden by subclasses
    }

    @Override
    public boolean isFilterable() {
        return true;   // True by default
    }

    public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle) {

        Rectangle rect = getDisplayableRect(trackRectangle, visibleRectangle);

        String trackName = getDisplayName();
        if ((trackName != null)) {

            if (rect.getHeight() > 3) {

                // Calculate fontsize
                int gap = Math.min(4, rect.height / 3);
                int fs = Math.min(fontSize, rect.height - gap);

                Font font = FontManager.getFont(fs);
                g2D.setFont(font);

                GraphicUtils.drawWrappedText(trackName, rect, g2D, false);

                //g2D.dispose();
            }
        }
    }


    protected int lastRenderY = -1;
    /**
     * Render the components which should only be rendered once
     * for a give y-value. We sometimes render tracks piecewise, but may
     * only want to show the scale once at the left, for instance
     * @param context
     * @param rect
     */

    /**
     * Return whether we have rendered anything at this Y-coordinate already
     * @param rect
     * @return
     */
    protected boolean isRepeatY(Rectangle rect){
        return rect.y == lastRenderY;
    }

    public void resetLastY(){
        this.lastRenderY = -1;
    }

    public void renderAttributes(Graphics2D graphics, Rectangle trackRectangle, Rectangle visibleRect,
                                 List<String> names, List<MouseableRegion> mouseRegions) {

        int x = trackRectangle.x;

        for (String name : names) {
            String key = name.toUpperCase();
            String attributeValue = getAttributeValue(key);
            if (attributeValue != null) {
                Rectangle rect = new Rectangle(x, trackRectangle.y, AttributeHeaderPanel.ATTRIBUTE_COLUMN_WIDTH,
                        trackRectangle.height);
                graphics.setColor(AttributeManager.getInstance().getColor(key, attributeValue));
                graphics.fill(rect);
                mouseRegions.add(new MouseableRegion(rect, key, attributeValue));

                // Border
//                graphics.setColor(Color.lightGray);
//                graphics.fillRect(x + AttributeHeaderPanel.ATTRIBUTE_COLUMN_WIDTH, trackRectangle.y,
//                        AttributeHeaderPanel.COLUMN_BORDER_WIDTH, trackRectangle.height);

            }
            x += AttributeHeaderPanel.ATTRIBUTE_COLUMN_WIDTH + AttributeHeaderPanel.COLUMN_BORDER_WIDTH;

        }
    }


    private Rectangle getDisplayableRect(Rectangle trackRectangle, Rectangle visibleRect) {
        Rectangle rect = null;
        if (visibleRect != null) {
            Rectangle intersectedRect = trackRectangle.intersection(visibleRect);
            if (intersectedRect.height > 15) {
                rect = intersectedRect;
            } else {
                rect = new Rectangle(trackRectangle);
            }
        }
        return rect;

    }


    /**
     * Called to overlay a track on another, presumably previously rendered,
     * track. The default behavior is to do nothing.
     *
     * @param context
     * @param rect
     */
    public void overlay(RenderContext context, Rectangle rect) {
    }


    public Color getColor() {
        return posColor;
    }

    public Color getAltColor() {
        return altColor;

    }

    public ResourceLocator getResourceLocator() {
        return resourceLocator;
    }

    public Collection<ResourceLocator> getResourceLocators() {
        return Arrays.asList(getResourceLocator());
    }

    /**
     * Add an attribute to this track and register the key with the attribute panel.
     * <p/>
     * Note:  Attribute keys are case insensitive.  Currently this is implemented
     * by forcing all keys to upper case
     *
     * @param name
     * @param value
     */
    public void setAttributeValue(String name, String value) {
        String key = name.toUpperCase();
        attributes.put(key, value);
        AttributeManager.getInstance().addAttribute(getSample(), name, value);
    }


    /**
     * Return the attribute value.  Attribute lookup occurs in the following order, if all fail null is returned.
     * <p/>
     * (1) the track attribute table
     * (2) by sampleId, as set in the Resource element of a session or load-from-server menu
     * (3) by track name, the visible display name
     * (4) by full path to the file associated with this track
     *
     * @param attributeName
     * @return
     */
    public String getAttributeValue(String attributeName) {
        String key = attributeName.toUpperCase();
        String value = attributes.get(key);
        final AttributeManager attributeManager = AttributeManager.getInstance();
        if (value == null && getSample() != null) {
            value = attributeManager.getAttribute(getSample(), key);
        }
        if (value == null) {
            value = attributeManager.getAttribute(getName(), key);
        }
        if (value == null && getResourceLocator() != null && getResourceLocator().getPath() != null) {
            value = attributeManager.getAttribute(getResourceLocator().getPath(), key);
        }
        return value;
    }

    public String getSample() {

        if (sampleId != null) {
            return sampleId;
        }
//        String sample = AttributeManager.getInstance().getSampleFor(getName());
//        return sample != null ? sample : getName();

        sampleId = AttributeManager.getInstance().getSampleFor(getName());
        return sampleId != null ? sampleId : getName();

    }


    /**
     * Returns the default height based on the default renderer for the data
     * type, as opposed to the actual renderer in use.  This is done to prevent
     * the track size from changing if renderer is changed.
     *
     * @return
     */
    private int getDefaultHeight() {
        if (XYPlotRenderer.class.isAssignableFrom(getDefaultRendererClass())) {
            return PreferenceManager.getInstance().getAsInt(PreferenceManager.CHART_TRACK_HEIGHT_KEY);
        } else {
            return PreferenceManager.getInstance().getAsInt(PreferenceManager.TRACK_HEIGHT_KEY);
        }
    }


    /**
     * Returns the default minimum height based on the actual renderer for this track.  Heatmaps default
     * to 1,  all other renderers to 5.
     *
     * @return
     */
    public int getDefaultMinimumHeight() {
        Renderer r = getRenderer();
        if (r != null && HeatmapRenderer.class.isAssignableFrom(r.getClass())) {
            return 1;
        } else {
            return 10;
        }
    }


    public void setMinimumHeight(int minimumHeight) {
        this.minimumHeight = minimumHeight;
    }

    public void setMaximumHeight(int maximumHeight) {
        this.maximumHeight = maximumHeight;
    }


    /**
     * Return the actual minimum height if one has been set, otherwise get the default for the current renderer.
     *
     * @return
     */
    public int getMinimumHeight() {
        return minimumHeight < 0 ? getDefaultMinimumHeight() : minimumHeight;
    }

    public int getMaximumHeight() {
        return maximumHeight;
    }

    public void setTrackType(TrackType type) {
        this.trackType = type;
    }


    public TrackType getTrackType() {
        return trackType;
    }

    public boolean isVisible() {

        if (visible && getTrackType() == TrackType.MUTATION) {
            // Special rules for mutations.  If display as overlays == true, only show if not overlaid on another
            // track and "show orphaned" is true
            boolean displayOverlays = IGV.getInstance().getSession().getOverlayMutationTracks();
            if (displayOverlays) {
                if (overlaid) {
                    return false;
                } else {
                    return PreferenceManager.getInstance().getAsBoolean(PreferenceManager.SHOW_ORPHANED_MUTATIONS);
                }
            }
        }
        return visible;
    }

    public void setColor(Color color) {
        this.posColor = color;
    }


    public void setAltColor(Color color) {
        altColor = color;
    }


    public void setVisible(boolean isVisible) {
        this.visible = isVisible;
    }


    public void setOverlayed(boolean bool) {
        this.overlaid = bool;
    }


    public void setSelected(boolean selected) {
        this.selected = selected;
    }


    public boolean isSelected() {
        return selected;
    }


    public void setHeight(int height) {
        setHeight(height, false);
    }

    @Override
    public void setHeight(int preferredHeight, boolean force) {
        if (height < getHeight()) {
            if ((this.getDisplayMode() == DisplayMode.EXPANDED) && (getTrackType() != TrackType.GENE)) {
                this.setDisplayMode(DisplayMode.SQUISHED);
            }
        }

        if (force) {
            this.height = preferredHeight;
        } else {
            this.height = Math.min(Math.max(getMinimumHeight(), preferredHeight), getMaximumHeight());
        }
    }

    public int getHeight() {
        return (height < 0) ? getDefaultHeight() : height;
    }

    public boolean hasDataRange() {
        return dataRange != null;
    }

    public DataRange getDataRange() {
        if (dataRange == null) {
            // Use the color scale if there is one
            float min = (float) (colorScale == null ? 0 : colorScale.getMinimum());
            float max = (float) (colorScale == null ? 10 : colorScale.getMaximum());
            float baseline = (float) (colorScale == null ? 0 : (colorScale.getNegStart() + colorScale.getPosStart()) / 2);

            setDataRange(new DataRange(min, baseline, max));
        }
        return dataRange;
    }


    public void setDataRange(DataRange axisDefinition) {
        this.dataRange = axisDefinition;
    }


    protected Class getDefaultRendererClass() {
        Class def = defaultRendererMap.get(getTrackType());
        return (def == null) ? defaultRendererClass : def;
    }

    public Collection<WindowFunction> getAvailableWindowFunctions() {
        return new ArrayList();
    }

    public boolean handleDataClick(TrackClickEvent te) {

        if (IGV.getInstance().isShowDetailsOnClick()) {
            return openTooltipWindow(te);
        }
        return false;
    }

    protected boolean openTooltipWindow(TrackClickEvent e) {
        ReferenceFrame frame = e.getFrame();
        final MouseEvent me = e.getMouseEvent();
        String popupText = getValueStringAt(frame.getChrName(), e.getChromosomePosition(), e.getMouseEvent().getY(), frame);

        if (popupText != null) {

            final TooltipTextFrame tf = new TooltipTextFrame(getName(), popupText);
            Point p = me.getComponent().getLocationOnScreen();
            tf.setLocation(Math.max(0, p.x + me.getX() - 150), Math.max(0, p.y + me.getY() - 150));

            UIUtilities.invokeOnEventThread(new Runnable() {
                public void run() {
                    tf.setVisible(true);
                }
            });
            return true;
        }
        return false;
    }

    public void handleNameClick(MouseEvent e) {
        // Do nothing
    }

    @Override
    public void setAutoScale(boolean autoScale) {
        this.autoScale = autoScale;
    }

    /**
     * Set some properties of this track,  usually from a "track line" specification.
     * <p/>
     * TODO -- keep the properties object, rather than copy all the values.
     *
     * @param properties
     */
    public void setProperties(TrackProperties properties) {
        this.itemRGB = properties.isItemRGB();
        this.useScore = properties.isUseScore();
        this.viewLimitMin = properties.getMinValue();
        this.viewLimitMax = properties.getMaxValue();
        this.yLine = properties.getyLine();
        this.drawYLine = properties.isDrawYLine();
        this.sortable = properties.isSortable();


        // If view limits are explicitly set turn off autoscale
        if (!Float.isNaN(viewLimitMin) && !Float.isNaN(viewLimitMax)) {
            this.setAutoScale(false);
        } else {
            this.setAutoScale(properties.isAutoScale());
        }

        // Color scale properties
        if (!properties.isAutoScale()) {

            float min = properties.getMinValue();
            float max = properties.getMaxValue();

            float mid = properties.getMidValue();
            if (Float.isNaN(mid)) {
                if (min >= 0) {
                    mid = Math.max(min, 0);
                } else {
                    mid = Math.min(max, 0);
                }
            }


            DataRange dr = new DataRange(min, mid, max);
            setDataRange(dr);

            if (properties.isLogScale()) {
                dr.setType(DataRange.Type.LOG);
            }

            // If the user has explicity set a data range and colors apply to heatmap as well
            Color maxColor = properties.getColor();
            Color minColor = properties.getAltColor();
            if (maxColor != null && minColor != null) {

                float tmp = properties.getNeutralFromValue();
                float neutralFrom = Float.isNaN(tmp) ? mid : tmp;
                tmp = properties.getNeutralToValue();
                float neutralTo = Float.isNaN(tmp) ? mid : tmp;

                Color midColor = properties.getMidColor();
                if (midColor == null) {
                    midColor = Color.white;
                }
                colorScale = new ContinuousColorScale(neutralFrom, min, neutralTo, max, minColor, midColor, maxColor);
            }

        }

        if (properties.getDisplayMode() != null) {
            this.setDisplayMode(properties.getDisplayMode());
        }

        if (properties.getName() != null) {
            name = properties.getName();
        }
        if (properties.getColor() != null) {
            setColor(properties.getColor());
        }
        if (properties.getAltColor() != null) {
            setAltColor(properties.getAltColor());
        }
        if (properties.getMidColor() != null) {
            //setMidColor(trackProperties.getMidColor());
        }
        if (properties.getHeight() > 0) {
            setHeight(properties.getHeight());
        }
        if (properties.getMinHeight() > 0) {
            setMinimumHeight(properties.getMinHeight());
        }
        if (properties.getRendererClass() != null) {
            setRendererClass(properties.getRendererClass());
        }
        if (properties.getWindowingFunction() != null) {
            setWindowFunction(properties.getWindowingFunction());
        }
        if (properties.getUrl() != null) {
            setUrl(properties.getUrl());
        }

        Map<String, String> attributes = properties.getAttributes();
        if (attributes != null) {
            for (Map.Entry<String, String> entry : attributes.entrySet()) {
                this.setAttributeValue(entry.getKey(), entry.getValue());
            }
        }

        // Start of Roche-Tessella modification
        this.autoScale = properties.getAutoScale();
        // End of Roche-Tessella modification

    }

    /**
     * @return the top
     */
    public int getY() {
        return top;
    }

    public void setColorScale(ContinuousColorScale colorScale) {
        this.colorScale = colorScale;
    }

    /**
     * @param top the top to set
     */
    public void setY(int top) {
        this.top = top;
    }

    /**
     * Return the color scale for this track.  If a specific scale exists for this data type
     * use that.  Otherwise create one using the track color and data range.
     *
     * @return
     */
    public ContinuousColorScale getColorScale() {
        if (colorScale == null) {

            if (IGV.hasInstance()) {
                ContinuousColorScale defaultScale = IGV.getInstance().getSession().getColorScale(trackType);
                if (defaultScale != null) {
                    return defaultScale;
                }
            }

            double min = dataRange == null ? 0 : dataRange.getMinimum();
            double max = dataRange == null ? 10 : dataRange.getMaximum();
            Color c = getColor();
            Color minColor = Color.white;
            if (min < 0) {
                minColor = altColor == null ? oppositeColor(minColor) : altColor;
                colorScale = new ContinuousColorScale(min, 0, max, minColor, Color.white, c);
            } else {
                colorScale = new ContinuousColorScale(min, max, minColor, c);
            }
            colorScale.setNoDataColor(UIConstants.NO_DATA_COLOR);
        }
        return colorScale;
    }

    private Color oppositeColor(Color c) {
        float[] rgb = new float[4];
        c.getRGBComponents(rgb);
        rgb[0] = Math.abs(rgb[0] - 255);
        rgb[1] = Math.abs(rgb[1] - 255);
        rgb[2] = Math.abs(rgb[2] - 255);
        return Color.getHSBColor(rgb[0], rgb[1], rgb[2]);
    }

    /**
     * Return the current state of this object as map of key-value pairs.  Used to store session state.
     * Only those attributes not already annotated in AbstractTrack need to be included here
     *
     * @return
     * @see #restorePersistentState
     */
    public Map<String, String> getPersistentState() {
        return new HashMap<String, String>();
    }


    /**
     * Restore from XML node. Default implementation just turns attributes
     * into a map
     *
     * @param node
     */
    public void restorePersistentState(Node node, int version) throws JAXBException {
        Map<String, String> attributes = Utilities.getAttributes(node);
        restorePersistentState(attributes, version);
    }

    /**
     * Restore attributes from track tag, no children
     * Only those attributes not unmarshalled (meaning not part of AbstractTrack)
     * need to be restored
     *
     * @param attributes
     * @see #getPersistentState
     */
    public void restorePersistentState(Map<String, String> attributes, int version) {

        String displayName = attributes.get(IGVSessionReader.SessionAttribute.DISPLAY_NAME.getText());
        String name = attributes.get(IGVSessionReader.SessionAttribute.NAME.getText());

        if (name != null && name.length() > 0) {
            setName(name);
        } else if (displayName != null && displayName.length() > 0) {
            setName(displayName);
        }

        // Set DataRange -- legacy (pre V3 sessions)
        if(version <= 3){
            String scale = attributes.get(IGVSessionReader.SessionAttribute.SCALE.getText());
            if (scale != null) {
                String[] axis = scale.split(",");
                float minimum = Float.parseFloat(axis[0]);
                float baseline = Float.parseFloat(axis[1]);
                float maximum = Float.parseFloat(axis[2]);
                setDataRange(new DataRange(minimum, baseline, maximum));
            }
        }

    }

    public boolean isItemRGB() {
        return itemRGB;
    }

    public boolean isUseScore() {
        return useScore;
    }

    public float getViewLimitMin() {
        return viewLimitMin;
    }

    public float getViewLimitMax() {
        return viewLimitMax;
    }

    public int getFontSize() {
        return fontSize;
    }

    public void setFontSize(int fontSize) {
        this.fontSize = fontSize;
    }

    public boolean isShowDataRange() {
        return showDataRange;
    }

    public void setShowDataRange(boolean showDataRange) {
        this.showDataRange = showDataRange;
    }


    /**
     * Overriden by subclasses
     *
     * @param e
     * @return
     */
    public Feature getFeatureAtMousePosition(TrackClickEvent e) {
        return null;
    }

    /**
     * Special normalization function for linear (non logged) copy number data
     *
     * @param value
     * @param norm
     * @return
     */
    public static float getLogNormalizedValue(float value, double norm) {
        if (norm == 0) {
            return Float.NaN;
        } else {
            return (float) (Math.log(Math.max(Float.MIN_VALUE, value) / norm) / Globals.log2);
        }
    }

    public float logScaleData(float dataY) {

        // Special case for copy # -- centers data around 2 copies (1 for allele
        // specific) and log normalizes
        if (((getTrackType() == TrackType.COPY_NUMBER) ||
                (getTrackType() == TrackType.ALLELE_SPECIFIC_COPY_NUMBER) ||
                (getTrackType() == TrackType.CNV)) &&
                !isLogNormalized()) {
            double centerValue = (getTrackType() == TrackType.ALLELE_SPECIFIC_COPY_NUMBER)
                    ? 1.0 : 2.0;

            dataY = getLogNormalizedValue(dataY, centerValue);
        }


        return dataY;
    }

    public boolean isRegionScoreType(RegionScoreType type) {
        return (getTrackType() == TrackType.GENE_EXPRESSION && type == RegionScoreType.EXPRESSION) ||
                ((getTrackType() == TrackType.COPY_NUMBER || getTrackType() == TrackType.CNV ||
                        getTrackType() == TrackType.ALLELE_SPECIFIC_COPY_NUMBER) &&
                        (type == RegionScoreType.AMPLIFICATION ||
                                type == RegionScoreType.DELETION ||
                                type == RegionScoreType.FLUX)) ||
                (type == RegionScoreType.MUTATION_COUNT) ||
                (type == RegionScoreType.SCORE);
    }

    public void setVisibilityWindow(int i) {
        this.visibilityWindow = i;
    }

    public int getVisibilityWindow() {
        return visibilityWindow;
    }

    /**
     * Override to return a specialized popup menu
     *
     * @return
     */
    public IGVPopupMenu getPopupMenu(final TrackClickEvent te) {
        return null;
    }

    public DisplayMode getDisplayMode() {
        return displayMode;
    }

    public void setDisplayMode(DisplayMode mode) {
        this.displayMode = mode;
    }


    public String getNameValueString(int y) {
        return getName();
    }

    /**
     * Return a value string for the tooltip window at the given location, or null to signal there is no value
     * at that location
     *
     * @param chr
     * @param position
     * @param y
     * @param frame
     * @return
     */
    public String getValueStringAt(String chr, double position, int y, ReferenceFrame frame) {
        return null;
    }


    public void setWindowFunction(WindowFunction type) {
        // Required method for track interface, ignore
    }

    @XmlAttribute(name = "windowFunction")
    public WindowFunction getWindowFunction() {
        return null;
    }

    public float getRegionScore(String chr, int start, int end, int zoom, RegionScoreType type, String frameName) {
        // Required method for track interface, ignore
        return getRegionScore(chr, start, end, zoom, type, frameName, null);
    }


    /**
     * @param chr
     * @param start
     * @param end
     * @param zoom
     * @param type
     * @param frameName
     * @param tracks
     * @return
     */
    public float getRegionScore(String chr, int start, int end, int zoom, RegionScoreType type, String frameName, List<Track> tracks) {
        // Required method for track interface, ignore
        return 0;
    }


    public boolean isLogNormalized() {
        // Required method for track interface, ignore
        return true;
    }

    public boolean isSortable() {
        return sortable;
    }

    public void setSortable(boolean sortable) {
        this.sortable = sortable;
    }

    public boolean isDrawYLine() {
        return drawYLine;
    }

    public float getYLine() {
        return yLine;
    }

    @Override
    public void dispose() {
        // Default is to do nothing.  Override in subclasses

    }


    protected void setRenderer(Renderer renderer) {
        //Here as setter for corresponding getter, subclasses should override
    }

    @XmlJavaTypeAdapter(SessionXmlAdapters.Renderer.class)
    @XmlAttribute(name = "renderer")
    @Override
    public Renderer getRenderer() {
        return null;
    }

    // Start of Roche-Tessella modification
    public boolean getAutoScale()    {
        return this.autoScale;
    }
    // End of Roche-Tessella modification
}
