/* * The MIT License (MIT) * * Copyright (c) 2007-2015 Broad Institute * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * FeatureTrackH5.java * * Created on November 12, 2007, 8:22 PM * * To change this template, choose Tools | Template Manager * and open the template in the editor. */ package org.broad.igv.track; import org.apache.log4j.Logger; import org.broad.igv.Globals; import org.broad.igv.feature.Chromosome; import org.broad.igv.feature.FeatureUtils; import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.DataRenderer; import org.broad.igv.renderer.GraphicUtils; import org.broad.igv.renderer.Renderer; import org.broad.igv.renderer.XYPlotRenderer; import org.broad.igv.session.IGVSessionReader; import org.broad.igv.session.SessionXmlAdapters; import org.broad.igv.session.SubtlyImportant; import org.broad.igv.ui.IGV; import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.ui.panel.FrameManager; import org.broad.igv.ui.panel.ReferenceFrame; import org.broad.igv.util.ResourceLocator; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.awt.*; import java.util.*; import java.util.List; /** * Represents a track of numeric data * * @author jrobinso */ @XmlType(factoryMethod = "getNextTrack") public abstract class DataTrack extends AbstractTrack implements ScalableTrack, IGVEventObserver { private static Logger log = Logger.getLogger(DataTrack.class); private DataRenderer renderer; private Map<String, LoadedDataInterval<List<LocusScore>>> loadedIntervalCache = new HashMap(200); public DataTrack(ResourceLocator locator, String id, String name) { super(locator, id, name); autoScale = PreferencesManager.getPreferences().getAsBoolean(Constants.CHART_AUTOSCALE); loadedIntervalCache = Collections.synchronizedMap(new HashMap<>()); IGVEventBus.getInstance().subscribe(FrameManager.ChangeEvent.class, this); } public void receiveEvent(Object event) { if (event instanceof FrameManager.ChangeEvent) { Collection<ReferenceFrame> frames = ((FrameManager.ChangeEvent) event).getFrames(); Map<String, LoadedDataInterval<List<LocusScore>>> newCache = Collections.synchronizedMap(new HashMap<>()); for (ReferenceFrame f : frames) { newCache.put(f.getName(), loadedIntervalCache.get(f.getName())); } loadedIntervalCache = newCache; } else { log.info("Unknown event type: " + event.getClass()); } } @Override public boolean isReadyToPaint(ReferenceFrame frame) { String chr = frame.getChrName(); int start = (int) frame.getOrigin(); int end = (int) frame.getEnd(); int zoom = frame.getZoom(); LoadedDataInterval interval = loadedIntervalCache.get(frame.getName()); return (interval != null && interval.contains(chr, start, end, zoom)); } public synchronized void load(ReferenceFrame referenceFrame) { if (isReadyToPaint(referenceFrame)) return; // already loaded String chr = referenceFrame.getChrName(); int start = (int) referenceFrame.getOrigin(); int end = (int) referenceFrame.getEnd() + 1; int zoom = referenceFrame.getZoom(); int maxEnd = end; Genome genome = GenomeManager.getInstance().getCurrentGenome(); String queryChr = chr; if (genome != null) { queryChr = genome.getCanonicalChrName(chr); Chromosome c = genome.getChromosome(chr); if (c != null) maxEnd = Math.max(c.getLength(), end); } // Expand interval +/- 50%, unless in a multi-locus mode with "lots" of frames boolean multiLocus = (FrameManager.getFrames().size() > 4); int delta = multiLocus ? 1 : (end - start) / 2; int expandedStart = Math.max(0, start - delta); int expandedEnd = Math.min(maxEnd, end + delta); LoadedDataInterval<List<LocusScore>> interval = getSummaryScores(queryChr, expandedStart, expandedEnd, zoom); loadedIntervalCache.put(referenceFrame.getName(), interval); } public void render(RenderContext context, Rectangle rect) { List<LocusScore> inViewScores = getInViewScores(context.getReferenceFrame()); if ((inViewScores == null || inViewScores.size() == 0) && Globals.CHR_ALL.equals(context.getChr())) { Graphics2D g = context.getGraphic2DForColor(Color.gray); GraphicUtils.drawCenteredText("Data not available for whole genome view; zoom in to see data", rect, g); } else { getRenderer().render(inViewScores, context, rect, this); } } public void overlay(RenderContext context, Rectangle rect) { List<LocusScore> inViewScores = getInViewScores(context.getReferenceFrame()); if ((inViewScores == null || inViewScores.size() == 0) && Globals.CHR_ALL.equals(context.getChr())) { Graphics2D g = context.getGraphic2DForColor(Color.gray); GraphicUtils.drawCenteredText("Data not available for whole genome view; select chromosome to see data", rect, g); } else if (inViewScores != null) { synchronized (inViewScores) { getRenderer().renderScores(this, inViewScores, context, rect); } } getRenderer().renderBorder(this, context, rect); } public List<LocusScore> getInViewScores(ReferenceFrame referenceFrame) { LoadedDataInterval<List<LocusScore>> interval = loadedIntervalCache.get(referenceFrame.getName()); String chr = referenceFrame.getChrName(); int start = (int) referenceFrame.getOrigin(); int end = (int) referenceFrame.getEnd() + 1; int zoom = referenceFrame.getZoom(); if (interval == null || !(chr.equals(interval.range.chr))) { // Try the data we have, even if not perfect !interval.contains(chr, start, end, zoom)) { return Collections.EMPTY_LIST; } List<LocusScore> inViewScores = interval.getFeatures(); // Trim scores int startIdx = FeatureUtils.getIndexBefore(start, inViewScores); int endIdx = inViewScores.size(); // Starting guess int tmp = FeatureUtils.getIndexBefore(end, inViewScores); if (tmp < 0) return Collections.EMPTY_LIST; else { for (int i = tmp; i < inViewScores.size(); i++) { if (inViewScores.get(i).getStart() > end) { endIdx = i + 1; break; } } endIdx = Math.max(startIdx + 1, endIdx); return startIdx == 0 && endIdx == inViewScores.size() - 1 ? inViewScores : inViewScores.subList(startIdx, endIdx); } } public Range getInViewRange(ReferenceFrame referenceFrame) { List<LocusScore> scores = getInViewScores(referenceFrame); if (scores.size() > 0) { float min = Float.MAX_VALUE; float max = -Float.MAX_VALUE; for (LocusScore score : scores) { float value = score.getScore(); if (!Float.isNaN(value)) { min = Math.min(value, min); max = Math.max(value, max); } } return new Range(min, max); } else { return null; } } public void clearCaches() { loadedIntervalCache.clear(); } public void setRendererClass(Class rc) { try { renderer = (DataRenderer) rc.newInstance(); } catch (Exception ex) { log.error("Error instantiating renderer ", ex); } } @Override protected void setRenderer(Renderer renderer) { this.renderer = (DataRenderer) renderer; } @XmlJavaTypeAdapter(SessionXmlAdapters.Renderer.class) @XmlAttribute(name = "renderer") @Override public DataRenderer getRenderer() { if (renderer == null) { setRendererClass(getDefaultRendererClass()); } return renderer; } /** * 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 mouseX * @param frame @return */ public String getValueStringAt(String chr, double position, int mouseX, int mouseY, ReferenceFrame frame) { StringBuffer buf = new StringBuffer(); LocusScore score = getLocusScoreAt(chr, position, frame); // If there is no value here, return null to signal no popup if (score == null) { return null; } buf.append(getName() + "<br>"); if ((getDataRange() != null) && (getRenderer() instanceof XYPlotRenderer)) { buf.append("Data scale: " + getDataRange().getMinimum() + " - " + getDataRange().getMaximum() + "<br>"); } buf.append(score.getValueString(position, mouseX, getWindowFunction())); return buf.toString(); } private LocusScore getLocusScoreAt(String chr, double position, ReferenceFrame frame) { int zoom = Math.max(0, frame.getZoom()); List<LocusScore> scores = getSummaryScores(chr, (int) position - 10, (int) position + 10, zoom).getFeatures(); if (scores == null) { return null; } else { // give a 2 pixel window, otherwise very narrow features will be missed. double bpPerPixel = frame.getScale(); int buffer = (int) (2 * bpPerPixel); /* * */ return (LocusScore) FeatureUtils.getFeatureAt(position, buffer, scores); } } abstract public LoadedDataInterval<List<LocusScore>> getSummaryScores(String chr, int startLocation, int endLocation, int zoom); /** * Get the score over the provided region for the given type. Different types * are processed differently. Results are cached according to the provided frameName, * if provided. If not, a string is created based on the inputs. * * @param chr * @param start * @param end * @param zoom * @param type * @param frameName * @param tracks Mutation scores require other tracks to calculate the score. If provided, * use these tracks. If null and not headless we use the currently loaded tracks. * @return */ public float getRegionScore(String chr, int start, int end, int zoom, RegionScoreType type, String frameName, List<Track> tracks) { if (end <= start) { return 0; } if (isRegionScoreType(type)) { List<LocusScore> scores = null; if (frameName == null) { //Essentially covering headless case here frameName = (chr + start) + end; frameName += zoom; frameName += type; } LoadedDataInterval<List<LocusScore>> loadedInterval = loadedIntervalCache.get(frameName); if (loadedInterval != null && loadedInterval.contains(chr, start, end, zoom)) { scores = loadedInterval.getFeatures(); } else { scores = this.getSummaryScores(chr, start, end, zoom).getFeatures(); loadedIntervalCache.put(frameName, new LoadedDataInterval<>(chr, start, end, zoom, scores)); } if (type == RegionScoreType.FLUX) { float sumDiffs = 0; float lastScore = Float.NaN; for (LocusScore score : scores) { if ((score.getEnd() >= start) && (score.getStart() <= end)) { if (Float.isNaN(lastScore)) { lastScore = Math.min(2, Math.max(-2, logScaleData(score.getScore()))); } else { float s = Math.min(2, Math.max(-2, logScaleData(score.getScore()))); sumDiffs += Math.abs(s - lastScore); lastScore = s; } } } return sumDiffs; } else if (type == RegionScoreType.MUTATION_COUNT) { // Sort by overlaid mutation count. if (!Globals.isHeadless() && tracks == null) { tracks = IGV.getInstance().getOverlayTracks(this); } float count = 0; String tSamp = this.getSample(); if (tracks != null && tSamp != null) { for (Track t : tracks) { if (t.getTrackType() == TrackType.MUTATION && tSamp.equals(t.getSample())) { count += t.getRegionScore(chr, start, end, zoom, type, frameName); } } } return count; } else { float regionScore = 0; int intervalSum = 0; boolean hasNan = false; for (LocusScore score : scores) { if ((score.getEnd() >= start) && (score.getStart() <= end)) { int interval = Math.min(end, score.getEnd()) - Math.max(start, score.getStart()); float value = score.getScore(); //For sorting it makes sense to skip NaNs. Not sure about other contexts if (Float.isNaN(value)) { hasNan = true; continue; } regionScore += value * interval; intervalSum += interval; } } if (intervalSum <= 0) { if (hasNan) { //If the only existing scores are NaN, the overall score should be NaN return Float.NaN; } else { // No scores in interval return -Float.MAX_VALUE; } } else { regionScore /= intervalSum; return (type == RegionScoreType.DELETION) ? -regionScore : regionScore; } } } else { return -Float.MAX_VALUE; } } /** * Return the average zcore over the interval. * * @param chr * @param start * @param end * @param zoom * @return */ public double getAverageScore(String chr, int start, int end, int zoom) { double regionScore = 0; int intervalSum = 0; Collection<LocusScore> scores = getSummaryScores(chr, start, end, zoom).getFeatures(); for (LocusScore score : scores) { if ((score.getEnd() >= start) && (score.getStart() <= end)) { int interval = 1; //Math.min(end, score.getEnd()) - Math.max(start, score.getStart()); float value = score.getScore(); regionScore += value * interval; intervalSum += interval; } } if (intervalSum > 0) { regionScore /= intervalSum; } return regionScore; } @SubtlyImportant private static DataTrack getNextTrack() { return (DataTrack) IGVSessionReader.getNextTrack(); } }