package de.uni_luebeck.inb.krabbenhoeft.eQTL.client.scroller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.logical.shared.HasSelectionHandlers;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;
import de.uni_luebeck.inb.krabbenhoeft.eQTL.api.gwt.GenomeRange;
import de.uni_luebeck.inb.krabbenhoeft.eQTL.client.scroller.GenomeDisplayTrack.Block;
import de.uni_luebeck.inb.krabbenhoeft.eQTL.client.scroller.RegisterForAutomation.HasAutomationHandlers;
public class GenomeDisplayScroller extends Widget implements HasValueChangeHandlers<GenomeRange>, HasSelectionHandlers<Long>, RequiresResize {
private final boolean transpose;
private final long blockCacheDistance = 4;
// cache blocks up to
// blockCacheDistance*pbPerPixel*512
// BasePairs away
private long contentLength;
private long scrollingPosition;
private long bpPerPixel;
private Map<Long, GenomeDisplayBlock512px> fromBP2displayBlock = new HashMap<Long, GenomeDisplayBlock512px>();
private List<GenomeDisplayBlock512px> displayBlocksCurrentlyVisible = new ArrayList<GenomeDisplayBlock512px>();
private GenomeDisplayTrack<?> tracks[];
private DivElement divCanvas;
private DivElement divContents;
private DivElement divScrollbar;
private DivElement divScrollbarSlider;
private DivElement divZoom;
private DivElement divZoomLevel[] = new DivElement[20];
private final native void setHoverHandlers(DivElement element) /*-{
element.onmouseover = function(){ element.className='hover'; };
element.onmouseout = function(){ element.className=''; };
}-*/;
public GenomeDisplayScroller(GenomeDisplayTrack<?> tracks[], String chromosome, boolean transpose, String baseId) {
this.transpose = transpose;
this.contentLength = 1;
this.tracks = tracks;
RegisterForAutomation.clearBaseId(baseId);
for (int i = 0; i < tracks.length; i++) {
GenomeDisplayTrack<?> genomeDisplayTrack = tracks[i];
genomeDisplayTrack.owner = this;
genomeDisplayTrack.transpose = transpose;
genomeDisplayTrack.autoId = RegisterForAutomation.register(baseId, new AutomationHandlers(i));
}
Document document = Document.get();
divCanvas = document.createDivElement();
setElement(divCanvas);
setStyleName("GenomeDisplayScroller");
divContents = document.createDivElement();
divContents.setClassName("GenomeDisplayScroller-contents");
divScrollbar = document.createDivElement();
divScrollbar.setClassName("GenomeDisplayScroller-scrollbar");
divScrollbarSlider = document.createDivElement();
divScrollbarSlider.setClassName("GenomeDisplayScroller-scrollbarSlider");
divScrollbar.appendChild(divScrollbarSlider);
divZoom = document.createDivElement();
divZoom.setClassName("GenomeDisplayScroller-zoom");
for (int i = 0; i < divZoomLevel.length; i++) {
divZoomLevel[i] = document.createDivElement();
setHoverHandlers(divZoomLevel[i]);
divZoom.appendChild(divZoomLevel[i]);
}
divCanvas.appendChild(divContents);
divCanvas.appendChild(divScrollbar);
divCanvas.appendChild(divZoom);
sinkEvents(Event.MOUSEEVENTS | Event.ONLOSECAPTURE | Event.ONCLICK);
changeChromosome(chromosome);
setZoomLevel(-1);
}
private String currentChromosome;
public void changeChromosome(String chromosome) {
currentChromosome = chromosome;
for (GenomeDisplayTrack<?> genomeDisplayTrack : tracks) {
genomeDisplayTrack.chromosome = chromosome;
genomeDisplayTrack.changedChromosome();
}
updateContentLength(1);
}
public void resize(int sx, int sy) {
resizeToOnMoveEnd = 0; // make sure there is no resize queued
final int zoomWidth = 20;
final int scrollbarHeight = 15;
if (sx == 0 && sy < 0) {
sx = transpose ? divCanvas.getClientHeight() : divCanvas.getClientWidth();
sy = -sy + scrollbarHeight;
}
if (sx < 300)
sx = 300;
if (sy < 50)
sy = 50;
if (!transpose) {
divContents.getStyle().setProperty("top", "0px");
divContents.getStyle().setProperty("left", "0px");
divContents.getStyle().setProperty("width", (sx - zoomWidth) + "px");
divContents.getStyle().setProperty("height", (sy - scrollbarHeight) + "px");
divScrollbar.getStyle().setProperty("top", (sy - scrollbarHeight) + "px");
divScrollbar.getStyle().setProperty("left", "0px");
divScrollbar.getStyle().setProperty("width", (sx - zoomWidth) + "px");
divScrollbar.getStyle().setProperty("height", scrollbarHeight + "px");
divZoom.getStyle().setProperty("top", "0px");
divZoom.getStyle().setProperty("left", (sx - zoomWidth + 2) + "px");
divZoom.getStyle().setProperty("width", zoomWidth + "px");
divZoom.getStyle().setProperty("height", sy + "px");
} else {
divContents.getStyle().setProperty("left", "0px");
divContents.getStyle().setProperty("top", "0px");
divContents.getStyle().setProperty("height", (sx - zoomWidth) + "px");
divContents.getStyle().setProperty("width", (sy - scrollbarHeight) + "px");
divScrollbar.getStyle().setProperty("left", (sy - scrollbarHeight) + "px");
divScrollbar.getStyle().setProperty("top", "0px");
divScrollbar.getStyle().setProperty("height", (sx - zoomWidth) + "px");
divScrollbar.getStyle().setProperty("width", scrollbarHeight + "px");
divZoom.getStyle().setProperty("left", "0px");
divZoom.getStyle().setProperty("top", (sx - zoomWidth + 2) + "px");
divZoom.getStyle().setProperty("height", zoomWidth + "px");
divZoom.getStyle().setProperty("width", sy + "px");
}
double scale = (double) sy / (double) (divZoomLevel.length);
int lastPos = 0;
for (int i = 0; i < divZoomLevel.length; i++) {
int curPos = (int) Math.round(((double) i + 1) * scale);
divZoomLevel[i].getStyle().setProperty("position", "absolute");
divZoomLevel[i].getStyle().setProperty(transpose ? "left" : "top", lastPos + "px");
divZoomLevel[i].getStyle().setProperty(transpose ? "width" : "height", (curPos - lastPos) + "px");
divZoomLevel[i].getStyle().setProperty(transpose ? "height" : "width", zoomWidth + "px");
lastPos = curPos;
}
}
@Override
protected void onLoad() {
DeferredCommand.addCommand(new Command() {
public void execute() {
if (transpose)
resize(divCanvas.getClientHeight(), divCanvas.getClientWidth());
else
resize(divCanvas.getClientWidth(), divCanvas.getClientHeight());
updateBlocks();
}
});
}
private void updateBlocks() {
int width = transpose ? divContents.getClientHeight() : divContents.getClientWidth();
if (width == 0)
return;
long blockWidthInBP = 512 * bpPerPixel;
long elementWidthInBP = width * bpPerPixel;
if (scrollingPosition > contentLength - elementWidthInBP - 1)
scrollingPosition = contentLength - elementWidthInBP - 1;
if (scrollingPosition < 0)
scrollingPosition = 0;
int scrollbarWidth = transpose ? divScrollbar.getClientHeight() : divScrollbar.getClientWidth();
divScrollbarSlider.getStyle().setProperty(transpose ? "top" : "left", scrollingPosition * scrollbarWidth / contentLength + "px");
divScrollbarSlider.getStyle().setProperty(transpose ? "height" : "width", Math.max(5, elementWidthInBP * scrollbarWidth / contentLength) + "px");
long lowBlock = (scrollingPosition / blockWidthInBP) * blockWidthInBP;
long highBlock = ((scrollingPosition + elementWidthInBP) / blockWidthInBP) * blockWidthInBP;
long cacheDist = blockCacheDistance * bpPerPixel * 512;
Iterator<Map.Entry<Long, GenomeDisplayBlock512px>> cb = fromBP2displayBlock.entrySet().iterator();
while (cb.hasNext()) {
Map.Entry<Long, GenomeDisplayBlock512px> e = cb.next();
if (e.getKey() + cacheDist < scrollingPosition || e.getKey() - cacheDist > scrollingPosition + elementWidthInBP) {
// out of cache region => remove
divContents.removeChild(e.getValue().contentsDiv);
cb.remove();
} else if (e.getKey() < lowBlock || e.getKey() > highBlock) {
// out of visible region => hide
e.getValue().contentsDiv.getStyle().setProperty("display", "none");
}
}
displayBlocksCurrentlyVisible.clear();
boolean needRecreate = false;
// NOTE: it is important to walk right to left and set zIndex so content
// can overflow correctly
int zindex = 0;
for (long i = highBlock; i >= lowBlock; i -= blockWidthInBP) {
GenomeDisplayBlock512px cur;
if (fromBP2displayBlock.containsKey(i)) {
cur = fromBP2displayBlock.get(i);
} else {
cur = new GenomeDisplayBlock512px(i, bpPerPixel, tracks.length);
needRecreate = true;
fromBP2displayBlock.put(i, cur);
divContents.appendChild(cur.contentsDiv);
}
displayBlocksCurrentlyVisible.add(cur);
cur.contentsDiv.getStyle().setProperty(transpose ? "top" : "left", (cur.fromBP - scrollingPosition) / bpPerPixel + "px");
cur.contentsDiv.getStyle().setProperty("display", "");
cur.contentsDiv.getStyle().setProperty("zIndex", "" + zindex++);
}
if (needRecreate)
updateTracks(false);
ValueChangeEvent.fire(this, new GenomeRange(currentChromosome, scrollingPosition, scrollingPosition + elementWidthInBP));
}
private int resizeToOnMoveEnd = 0;
@SuppressWarnings("unchecked")
public void updateTracks(boolean bForceUpdate) {
int topY = 0;
boolean moveFollowing = bForceUpdate;
for (int tid = 0; tid < tracks.length; tid++) {
int maxBottomY = topY;
for (GenomeDisplayBlock512px cur : displayBlocksCurrentlyVisible) {
GenomeDisplayTrack.Block b = cur.blockCache[tid];
if (b != null && b.html != null && !moveFollowing) {
// if we have a cache and we are NOT moving down, use
// cache
maxBottomY = Math.max(maxBottomY, b.bottomY);
continue;
}
b = tracks[tid].renderBlock512px(cur.fromBP, cur.bpPerPixel, topY, b);
cur.blockCache[tid] = b;
maxBottomY = Math.max(maxBottomY, b.bottomY);
}
// if the size of our blocks does not equal the cached blocks,
// we need to recalculate all blocks below the current one
if (maxBottomY != tracks[tid].bottomYCacheForScroller) {
moveFollowing = true;
tracks[tid].bottomYCacheForScroller = maxBottomY;
}
topY = maxBottomY;
}
if (moveFollowing) {
if (!moving)
resize(0, -topY);
else
resizeToOnMoveEnd = -topY;
}
for (GenomeDisplayBlock512px cur : displayBlocksCurrentlyVisible) {
boolean blockLoading = false;
String html = "";
for (int tid = 0; tid < tracks.length; tid++) {
if (cur.blockCache[tid].html != null)
html += cur.blockCache[tid].html;
else
blockLoading = true;
}
if (blockLoading) {
if (transpose)
html += "<div style=\"position: absolute; z-index: 101; left: 0px; top: 0px; width: 20px; height: 512px; background-color: white; \"> loading ... </div>";
else
html += "<div style=\"position: absolute; z-index: 101; left: 0px; top: 0px; width: 512px; height: 20px; background-color: white; \"> loading ... </div>";
}
cur.setContent(topY, html);
}
}
private boolean updateUseful = false;
public void scheduleUpdate() {
updateUseful = true;
new Timer() {
@Override
public void run() {
if (updateUseful) {
updateUseful = false;
updateTracks(true);
}
}
}.schedule(100);
}
public void updateContentLength(long newLength) {
contentLength = newLength;
for (GenomeDisplayBlock512px cur : fromBP2displayBlock.values()) {
divContents.removeChild(cur.contentsDiv);
}
fromBP2displayBlock.clear();
updateBlocks();
}
public void setZoomLevel(long zoom) {
if (bpPerPixel == zoom)
return;
bpPerPixel = zoom;
if (bpPerPixel < 1 || bpPerPixel > 1000 * 1000)
bpPerPixel = 8192 << 6;
for (int i = 0; i < divZoomLevel.length; i++) {
divZoomLevel[i].getStyle().setProperty("backgroundColor", (((long) 1) << i) == bpPerPixel ? "black" : "");
}
updateContentLength(contentLength);
SelectionEvent.fire(this, zoom);
}
private boolean moving = false;
private boolean dragging = false;
private int movingStartX = 0;
private long movingStartScroll;
private void doScrollOrDrag(int curx) {
if (dragging) {
scrollingPosition = movingStartScroll + (movingStartX - curx) * bpPerPixel;
} else {
int w = transpose ? divScrollbar.getClientHeight() : divScrollbar.getClientWidth();
scrollingPosition = movingStartScroll + (curx - movingStartX) * contentLength / w;
}
}
public void scrollRelative(int offset) {
scrollingPosition += offset * bpPerPixel;
updateBlocks();
}
@Override
public void onBrowserEvent(Event event) {
Element target = DOM.eventGetTarget(event);
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEDOWN: {
movingStartX = transpose ? (DOM.eventGetClientY(event) - getAbsoluteTop()) : (DOM.eventGetClientX(event) - getAbsoluteLeft());
movingStartScroll = scrollingPosition;
moving = true;
if (DOM.isOrHasChild((Element) divContents.cast(), target))
dragging = true;
else
dragging = false;
DOM.setCapture((Element) (dragging ? divContents : divScrollbar).cast());
DOM.eventPreventDefault(event);
break;
}
case Event.ONMOUSEUP: {
if (moving) {
// The order of these two lines is important. If we release
// capture
// first, then we might trigger an onLoseCapture event before we
// set
// isResizing to false.
moving = false;
if (resizeToOnMoveEnd != 0)
resize(0, resizeToOnMoveEnd);
DOM.releaseCapture((Element) (dragging ? divContents : divScrollbar).cast());
}
break;
}
case Event.ONMOUSEMOVE: {
if (moving) {
assert DOM.getCaptureElement() != null;
doScrollOrDrag(transpose ? (DOM.eventGetClientY(event) - getAbsoluteTop()) : (DOM.eventGetClientX(event) - getAbsoluteLeft()));
updateBlocks();
DOM.eventPreventDefault(event);
}
break;
}
// IE automatically releases capture if the user switches windows,
// so we
// need to catch the event and stop resizing.
case Event.ONLOSECAPTURE: {
moving = false;
if (resizeToOnMoveEnd != 0)
resize(0, resizeToOnMoveEnd);
break;
}
case Event.ONCLICK: {
for (int i = 0; i < divZoomLevel.length; i++) {
if (DOM.isOrHasChild((Element) divZoomLevel[i].cast(), target))
setZoomLevel(((long) 1) << i);
}
break;
}
}
super.onBrowserEvent(event);
}
public List<GenomeDisplayTrack.Block<?>> collectVisibleBlocksForTrack(int trackId) {
List<GenomeDisplayTrack.Block<?>> ret = new ArrayList<GenomeDisplayTrack.Block<?>>();
for (GenomeDisplayBlock512px cur : displayBlocksCurrentlyVisible) {
Block<?> o = cur.blockCache[trackId];
if (o == null)
return null;
ret.add(o);
}
return ret;
}
private HandlerManager handlers = new HandlerManager(null);
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<GenomeRange> handler) {
return handlers.addHandler(ValueChangeEvent.getType(), handler);
}
public HandlerRegistration addSelectionHandler(SelectionHandler<Long> handler) {
return handlers.addHandler(SelectionEvent.getType(), handler);
}
public void fireEvent(GwtEvent<?> event) {
handlers.fireEvent(event);
}
public class AutomationHandlers implements HasAutomationHandlers {
final int trackId;
public AutomationHandlers(int i) {
trackId = i;
}
private Object getObject(String fromBP, int itemIndex) {
return fromBP2displayBlock.get(Long.parseLong(fromBP)).blockCache[trackId].data[itemIndex];
}
public void onMouseClick(String fromBP, int itemIndex) {
tracks[trackId].onMouseClick(getObject(fromBP, itemIndex));
}
public void onMouseOut(String fromBP, int itemIndex) {
tracks[trackId].onMouseOut(getObject(fromBP, itemIndex));
}
public void onMouseOver(String fromBP, int itemIndex) {
tracks[trackId].onMouseOver(getObject(fromBP, itemIndex));
}
}
public void onResize() {
updateTracks(true);
}
}