/* * 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. */ /* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.broad.igv.ui.panel; import org.apache.log4j.Logger; import org.broad.igv.Globals; import org.broad.igv.event.ShiftEvent; import org.broad.igv.feature.Chromosome; import org.broad.igv.feature.Locus; import org.broad.igv.feature.Range; 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.sam.InsertionManager; import org.broad.igv.sam.InsertionMarker; import org.broad.igv.ui.IGV; import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.ViewChange; import org.broad.igv.ui.util.MessageUtils; /** * @author jrobinso */ public class ReferenceFrame { private static Logger log = Logger.getLogger(ReferenceFrame.class); IGVEventBus eventBus; /** * The origin in bp */ public volatile double origin = 0; /** * The nominal viewport width in pixels. */ public static int binsPerTile = 700; boolean visible = true; private String name; /** * The chromosome currently in view */ protected String chrName = "chrAll"; /** * The minimum zoom level for the current screen size + chromosome combination. */ private int minZoom = 0; /** * The maximum zoom level. Set to prevent integer overflow. This is a function * of chromosome length. */ public int maxZoom = 23; /** * Minimum allowed range in base-pairs */ protected static final int minBP = 40; /** * The current zoom level. Zoom level -1 corresponds to the whole * genome view (chromosome "all") */ protected int zoom = minZoom; /** * X location of the frame in pixels */ volatile int pixelX; /** * Width of the frame in pixels */ protected int widthInPixels; /** * The number of tiles for this zoom level, = 2^zoom */ protected double nTiles = 1; /** * The location (x axis) locationScale in base pairs / virtual pixel */ protected volatile double scale; protected Locus initialLocus = null; public ReferenceFrame(String name) { this.name = name; Genome genome = getGenome(); this.chrName = genome == null ? "" : genome.getHomeChromosome(); this.eventBus = IGVEventBus.getInstance(); } public ReferenceFrame(ReferenceFrame otherFrame) { this(otherFrame, otherFrame.eventBus); } /** * Copy constructor with event bus ovverride -- used by Sashimii plot * * @param otherFrame */ public ReferenceFrame(ReferenceFrame otherFrame, IGVEventBus eventBus) { this.chrName = otherFrame.chrName; this.initialLocus = otherFrame.initialLocus; this.scale = otherFrame.scale; this.minZoom = otherFrame.minZoom; this.name = otherFrame.name; this.nTiles = otherFrame.nTiles; this.origin = otherFrame.origin; this.pixelX = otherFrame.pixelX; this.widthInPixels = otherFrame.widthInPixels; this.zoom = otherFrame.zoom; this.maxZoom = otherFrame.maxZoom; this.eventBus = eventBus; } public boolean isVisible() { return visible; } public void setVisible(boolean visible) { this.visible = visible; } public void dragStopped() { setOrigin(Math.round(origin)); // Snap to gride eventBus.post(ViewChange.Result()); } public void changeGenome(Genome genome) { setChromosomeName(genome.getHomeChromosome(), true); } public void changeChromosome(String chrName, boolean recordHistory) { boolean changed = setChromosomeName(chrName, false); // if (changed) { ViewChange resultEvent = ViewChange.ChromosomeChangeResult(chrName); resultEvent.setRecordHistory(recordHistory); eventBus.post(resultEvent); changeZoom(0); // } } public void changeZoom(int newZoom) { doSetZoom(newZoom); ViewChange result = ViewChange.Result(); result.setRecordHistory(false); eventBus.post(result); } /** * Set the position and width of the frame, in pixels * The origin/end positions are kept fixed iff valid * * @param pixelX * @param widthInPixels */ public synchronized void setBounds(int pixelX, int widthInPixels) { this.pixelX = pixelX; if (this.widthInPixels != widthInPixels) { //If we have what looks like a valid end position we keep it if (this.widthInPixels > 0 && this.initialLocus == null) { int start = (int) getOrigin(); int end = (int) getEnd(); if (start >= 0 && end >= 1) { this.initialLocus = new Locus(getChrName(), start, end); } } this.widthInPixels = widthInPixels; computeLocationScale(); computeZoom(); } } /** * Sets zoom level and recomputes scale, iff newZoom != oldZoom * min/maxZoom are recalculated and respected, * and the locationScale is recomputed * * @param newZoom */ protected void setZoom(int newZoom) { if (zoom != newZoom) { synchronized (this) { setZoomWithinLimits(newZoom); computeLocationScale(); } } } /** * Set the origin of the frame, guarding against chromosome boundaries * * @param position */ public void setOrigin(double position) { int windowLengthBP = (int) (widthInPixels * getScale()); double newOrigin; if (PreferencesManager.getPreferences().getAsBoolean(Constants.SAM_SHOW_SOFT_CLIPPED)) { newOrigin = Math.max(-1000, Math.min(position, getMaxCoordinate() + 1000 - windowLengthBP)); } else { newOrigin = Math.max(0, Math.min(position, getMaxCoordinate() - windowLengthBP)); } origin = newOrigin; } protected synchronized void setZoomWithinLimits(int newZoom) { zoom = Math.max(minZoom, Math.min(maxZoom, newZoom)); nTiles = Math.pow(2, zoom); } /** * Increment the zoom level by {@code zoomIncrement}, leaving * the center the same * * @param zoomIncrement */ public void doZoomIncrement(int zoomIncrement) { double currentCenter = getGenomeCenterPosition(); doIncrementZoom(zoomIncrement, currentCenter); } /** * Set the zoom level to {@code newZoom}, leaving * the center the same * * @param newZoom */ public void doSetZoom(int newZoom) { double currentCenter = getGenomeCenterPosition(); doSetZoomCenter(newZoom, currentCenter); } public void doIncrementZoom(final int zoomIncrement, final double newCenter) { doSetZoomCenter(getZoom() + zoomIncrement, newCenter); } /** * Intended to be called by UI elements, this method * performs all actions necessary to set a new zoom * and center location * * @param newZoom * @param newCenter Center position, in genome coordinates */ public void doSetZoomCenter(final int newZoom, final double newCenter) { if (chrName.equals(Globals.CHR_ALL)) { chrName = getGenome().getHomeChromosome(); } if (!chrName.equals(Globals.CHR_ALL)) { setZoom(newZoom); // Adjust origin so newCenter is centered centerOnLocation(newCenter); } } protected double getGenomeCenterPosition() { return origin + ((widthInPixels / 2) * getScale()); } /** * Return the current locationScale in base pairs / pixel * * @return */ public double getScale() { if (scale <= 0) { computeLocationScale(); } return scale; } /** * Calls {@link #setChromosomeName(String, boolean)} with force = false * It is preferred that you post an event to the EventBus instead, this is public * as an implementation side effect * * @param name * @return boolean indicating whether the chromosome actually changed */ public boolean setChromosomeName(String name) { return setChromosomeName(name, false); } /** * Change the frame to the specified chromosome, clearing all * view parameters (zoom, locationScale) in the process * * @param name Name of the new chromosome * @param force Whether to force a change to the new chromosome, even if it's * the same name as the old one * @return boolean indicating whether the chromosome actually changed */ public synchronized boolean setChromosomeName(String name, boolean force) { if (shouldChangeChromosome(name) || force) { chrName = name; origin = 0; this.scale = -1; this.calculateMaxZoom(); this.zoom = -1; setZoom(0); //chromoObservable.setChangedAndNotify(); return true; } return false; } /** * Record the current state of the frame in history. * It is recommended that this NOT be called from within ReferenceFrame, * and callers use it after making all changes * <p> * //TODO Should we save history by receiving events in History? */ public void recordHistory() { IGV.getInstance().getSession().getHistory().push(getFormattedLocusString(), zoom); } public void shiftOriginPixels(int delta) { double shiftBP = delta * getScale(); setOrigin(origin + shiftBP); eventBus.post(ViewChange.Result()); } public void centerOnLocation(String chr, double chrLocation) { if (!chrName.equals(chr)) { setChromosomeName(chr); } centerOnLocation(chrLocation); } public void centerOnLocation(double chrLocation) { double windowWidth = (widthInPixels * getScale()) / 2; setOrigin(Math.round(chrLocation - windowWidth)); eventBus.post(ViewChange.LocusChangeResult(chrName, origin, chrLocation + windowWidth)); } public boolean windowAtEnd() { double windowLengthBP = widthInPixels * getScale(); return origin + windowLengthBP + 1 > getMaxCoordinate(); } /** * Move the frame to the specified position. New zoom is calculated * based on limits. * * @param chr * @param start * @param end */ public void jumpTo(String chr, int start, int end) { Locus locus = new Locus(chr, start, end); this.jumpTo(locus); } public void jumpTo(Locus locus) { String chr = locus.getChr(); int start = locus.getStart(); int end = locus.getEnd(); Genome genome = getGenome(); if (chr != null) { if (genome.getChromosome(chr) == null && !chr.contains(Globals.CHR_ALL)) { MessageUtils.showMessage(chr + " is not a valid chromosome."); return; } } end = Math.min(getMaxCoordinate(chr), end); synchronized (this) { this.initialLocus = locus; this.chrName = chr; if (start >= 0 && end >= 0) { log.info("this.origin = " + start); this.origin = start; beforeScaleZoom(locus); computeLocationScale(); computeZoom(); } } if (log.isDebugEnabled()) { log.debug("Data panel width = " + widthInPixels); log.debug("New start = " + (int) origin); log.debug("New end = " + (int) getEnd()); log.debug("New center = " + (int) getCenter()); log.debug("Scale = " + scale); } eventBus.post(ViewChange.LocusChangeResult(chrName, start, end)); } public double getOrigin() { return origin; } public double getCenter() { return origin + getScale() * widthInPixels / 2; } public double getEnd() { return origin + getScale() * widthInPixels; } public int getZoom() { return zoom; } /** * Return the maximum zoom level * * @return */ public int getMaxZoom() { return maxZoom; } public int getAdjustedZoom() { return zoom - minZoom; } /** * Determine if this view will change at all based on the {@code newChrName} * The view changes if newChrName != {@code #this.chr} or if we are not * at full chromosome view * * @param newChrName * @return */ private boolean shouldChangeChromosome(String newChrName) { return chrName == null || !chrName.equals(newChrName); } protected void calculateMaxZoom() { this.maxZoom = Globals.CHR_ALL.equals(this.chrName) ? 0 : (int) Math.ceil(Math.log(getChromosomeLength() / minBP) / Globals.log2); } public String getChrName() { return chrName; } // TODO -- this parameter shouldn't be stored here. Maybe in a specialized // layout manager? public int getWidthInPixels() { return widthInPixels; } /** * Return the chromosome position corresponding to the pixel index. The * pixel index is the pixel "position" translated by -origin. * * @param screenPosition * @return */ public double getChromosomePosition(int screenPosition) { InsertionMarker i = InsertionManager.getInstance().getSelectedInsertion(getChrName()); if (i != null && i.position > origin) { // if (IGV.getInstance().getSession().expandInsertions && insertionMarkers != null && insertionMarkers.size() > 0) { double start = getOrigin(); double scale = getScale(); double iEnd = 0, iStart = 0; iStart = iEnd + (i.position - start) / scale; // Screen position of insertionMarker start if (screenPosition < iStart) { return start + scale * (screenPosition - iEnd); } iEnd = iStart + i.size / scale; // Screen position of insertionMarker end if (screenPosition < iEnd) { return i.position; // In the gap } start = i.position + 1; // } return start + scale * (screenPosition - iEnd); } else { return origin + getScale() * screenPosition; } } /** * Return the screen position corresponding to the chromosomal position. * * @param chromosomePosition * @return */ public int getScreenPosition(double chromosomePosition) { InsertionMarker i = InsertionManager.getInstance().getSelectedInsertion(chrName); if (i == null || i.position < origin || i.position > chromosomePosition) { return (int) ((chromosomePosition - origin) / getScale()); } else { return (int) ((chromosomePosition + i.size - origin) / getScale()); } } public Chromosome getChromosome() { Genome genome = getGenome(); if (genome == null) { return null; } return genome.getChromosome(chrName); } /** * The maximum coordinate currently allowed. * In genomic coordinates this is the same as the chromosome length. * In exome coordinates, the two are different * (since ExomeReferenceFrame takes input in genomic coordinates) * * @return * @see #getChromosomeLength() */ public int getMaxCoordinate() { return this.getChromosomeLength(); } private static int getMaxCoordinate(String chrName) { return getChromosomeLength(chrName); } /** * Chromosome length, in genomic coordinates. * Intended to be used for scaling * * @return * @see #getMaxCoordinate() */ public int getChromosomeLength() { return getChromosomeLength(this.chrName); } public double getTilesTimesBinsPerTile() { return nTiles * (double) binsPerTile; } public int getMidpoint() { return pixelX + widthInPixels / 2; } /** * Get the UCSC style locus string corresponding to the current view. THe UCSC * conventions are followed for coordinates, specifically the internal representation * is "zero" based (first base is numbered 0) but the display representation is * "one" based (first base is numbered 1). Consequently 1 is added to the * computed positions. * * @return */ public String getFormattedLocusString() { if (zoom == 0) { return getChrName(); } else { Range range = getCurrentRange(); return Locus.getFormattedLocusString(range.getChr(), range.getStart(), range.getEnd()); } } public Range getCurrentRange() { int start = 0; int end = widthInPixels; int startLoc = (int) getChromosomePosition(start) + 1; int endLoc = (int) getChromosomePosition(end); Range range = new Range(getChrName(), startLoc, endLoc); return range; } public void reset() { jumpTo(FrameManager.getLocus(name)); } public String getName() { return name; } public Locus getInitialLocus() { return initialLocus; } public int getMinZoom() { return minZoom; } public void setName(String name) { this.name = name; } public int getStateHash() { return (chrName + origin + scale + widthInPixels).hashCode(); } /** * Recalculate the locationScale, based on {@link #initialLocus}, {@link #origin}, and * {@link #widthInPixels} * DOES NOT alter zoom value */ private synchronized void computeLocationScale() { Genome genome = getGenome(); //Should consider getting rid of this. We don't have //a chromosome length without a genome, not always a problem if (genome != null) { // The end location, in base pairs. // If negative, we use the whole chromosome int setEnd = -1; if (this.initialLocus != null) setEnd = this.initialLocus.getEnd(); if (setEnd > 0 && widthInPixels > 0) { this.scale = ((setEnd - origin) / widthInPixels); this.initialLocus = null; } else { double virtualPixelSize = getTilesTimesBinsPerTile(); double nPixel = Math.max(virtualPixelSize, widthInPixels); this.scale = (((double) getChromosomeLength()) / nPixel); } } } /** * Recalculate the zoom value based on current start/end * locationScale is not altered */ private void computeZoom() { int newZoom = calculateZoom(getOrigin(), getEnd()); setZoomWithinLimits(newZoom); } /** * Called before scaling and zooming, during jumpTo. * Intended to be overridden * * @param locus */ private void beforeScaleZoom(Locus locus) { calculateMaxZoom(); } /** * Calculate the zoom level given start/end in bp. * Doesn't change anything * * @param start * @param end * @return */ private int calculateZoom(double start, double end) { return (int) Math.round((Math.log((getChromosomeLength() / (end - start)) * (((double) widthInPixels) / binsPerTile)) / Globals.log2)); } private static int getChromosomeLength(String chrName) { Genome genome = getGenome(); if (genome == null) { return 1; } if (chrName.equals("All")) { // Genome coordinates are in kb => divde by 1000 return (int) (genome.getNominalLength() / 1000); } else { Chromosome chromosome = genome.getChromosome(chrName); if (chromosome == null) { log.error("Null chromosome: " + chrName); if (genome.getChromosomes().size() == 0) { return 1; } else { return genome.getChromosomes().iterator().next().getLength(); } } return chromosome.getLength(); } } private static Genome getGenome() { return GenomeManager.getInstance().getCurrentGenome(); } }