oncoprintjs
Version:
A data visualization for cancer genomic data.
1,317 lines (1,224 loc) • 62.5 kB
text/typescript
import gl_matrix from 'gl-matrix';
import OncoprintZoomSlider from './oncoprintzoomslider';
import $ from 'jquery';
import zoomToFitIcon from '../img/zoomtofit.svg';
import OncoprintModel, { ColumnIndex, TrackId } from './oncoprintmodel';
import OncoprintWebGLCellView, {
OncoprintShaderProgram,
OncoprintTrackBuffer,
OncoprintVertexTrackBuffer,
OncoprintWebGLContext,
} from './oncoprintwebglcellview';
import MouseMoveEvent = JQuery.MouseMoveEvent;
import { clamp, cloneShallow } from './utils';
import _ from 'lodash';
export type MinimapViewportSpec = {
left_col: ColumnIndex; // leftmost col included in viewport
right_col: ColumnIndex; // col at the boundary of viewport, first col to the right of viewport
scroll_y_proportion: number; // between 0 and 1
zoom_y: number; // between 0 and 1
};
type OverlayRectParams = {
top: number;
left_col: ColumnIndex;
right_col: ColumnIndex;
height: number;
};
class OverlayRectSpec {
constructor(
private params: OverlayRectParams,
private getCellWidth: () => number
) {}
public setParams(p: OverlayRectParams) {
this.params = p;
}
public clone() {
return new OverlayRectSpec(
cloneShallow(this.params),
this.getCellWidth
);
}
public get left_col() {
return this.params.left_col;
}
public get right_col() {
return this.params.right_col;
}
public get top() {
return this.params.top;
}
public get height() {
return this.params.height;
}
public get num_cols() {
return this.right_col - this.left_col;
}
public get left() {
return this.left_col * this.getCellWidth();
}
public get right() {
return this.right_col * this.getCellWidth();
}
public get width() {
return this.right - this.left;
}
}
export default class OncoprintMinimapView {
private handleContextLost: () => void;
private layout_numbers = {
window_width: -1,
window_height: -1,
vertical_zoom_area_width: -1,
horizontal_zoom_area_height: -1,
padding: -1,
window_bar_height: -1,
canvas_left: -1,
canvas_top: -1,
};
private current_rect: OverlayRectSpec;
private $window_bar: JQuery;
private $close_btn: JQuery;
private horizontal_zoom: OncoprintZoomSlider;
private vertical_zoom: OncoprintZoomSlider;
private ctx: OncoprintWebGLContext | null;
private ext: ANGLE_instanced_arrays | null;
private overlay_ctx: CanvasRenderingContext2D | null;
private pMatrix: any;
private mvMatrix: any;
private shader_program: OncoprintShaderProgram;
private resize_hover:
| 'r'
| 'l'
| 't'
| 'b'
| 'tl'
| 'br'
| 'bl'
| 'tr'
| false = false;
private rendering_suppressed = false;
private visible = false;
constructor(
private $div: JQuery,
private $canvas: JQuery<HTMLCanvasElement>,
private $overlay_canvas: JQuery<HTMLCanvasElement>,
model: OncoprintModel,
cell_view: OncoprintWebGLCellView,
width: number,
height: number,
drag_callback: (x: number, y: number) => void,
viewport_callback: (vp: MinimapViewportSpec) => void,
horz_zoom_callback: (z: number) => void,
vert_zoom_callback: (z: number) => void,
zoom_to_fit_callback: () => void,
close_callback: () => void
) {
this.$div = $div;
this.$canvas = $canvas;
this.$overlay_canvas = $overlay_canvas;
this.current_rect = new OverlayRectSpec(
{ top: 0, left_col: 0, right_col: 0, height: 0 },
() => model.getCellWidth(true) * this.getZoom(model).x
);
const self = this;
const padding = 4;
const vertical_zoom_area_width = 20;
const horizontal_zoom_area_height = 20;
const window_bar_height = 20;
this.handleContextLost = function() {
// catch when context lost and refresh it
// eg if cell view uses a ton of contexts, then browser clears oldest context,
// then the minimap would be empty until we refresh the context and rerender
self.drawOncoprintAndOverlayRect(model, cell_view);
}.bind(this);
this.$canvas[0].addEventListener(
'webglcontextlost',
this.handleContextLost
);
this.layout_numbers = {
window_width: padding + width + padding + vertical_zoom_area_width,
window_height:
window_bar_height +
padding +
height +
padding +
horizontal_zoom_area_height,
vertical_zoom_area_width: vertical_zoom_area_width,
horizontal_zoom_area_height: horizontal_zoom_area_height,
padding: padding,
window_bar_height: window_bar_height,
canvas_left: padding,
canvas_top: window_bar_height + padding,
};
this.$div.css({
'min-width': this.layout_numbers.window_width,
'min-height': this.layout_numbers.window_height,
outline: 'solid 1px black',
'background-color': '#ffffff',
});
this.$window_bar = $('<div>')
.css({
position: 'absolute',
'min-width': this.layout_numbers.window_width,
'min-height': this.layout_numbers.window_bar_height,
'background-color': '#cccccc',
})
.appendTo(this.$div);
this.$close_btn = $('<div>')
.css({
position: 'absolute',
top: 3,
left: 3,
'min-width': this.layout_numbers.window_bar_height - 6,
'min-height': this.layout_numbers.window_bar_height - 6,
cursor: 'pointer',
})
.appendTo(this.$div);
$('<span>')
.addClass('icon fa fa-times-circle')
.css('font-size', this.layout_numbers.window_bar_height - 6 + 'px')
.appendTo(this.$close_btn);
this.$close_btn.click(close_callback || function() {});
this.$canvas[0].width = width;
this.$canvas[0].height = height;
this.$canvas.css({
top: this.layout_numbers.canvas_top,
left: this.layout_numbers.canvas_left,
});
this.$overlay_canvas[0].width = width;
this.$overlay_canvas[0].height = width;
this.$overlay_canvas.css({
top: this.layout_numbers.canvas_top,
left: this.layout_numbers.canvas_left,
outline: 'solid 1px #444444',
});
this.horizontal_zoom = new OncoprintZoomSlider(this.$div, {
btn_size: this.layout_numbers.horizontal_zoom_area_height - padding,
horizontal: true,
width: width,
init_val: model.getHorzZoom(),
left: padding,
top: this.layout_numbers.canvas_top + height + padding,
onChange: function(val) {
horz_zoom_callback(val);
},
});
this.vertical_zoom = new OncoprintZoomSlider(this.$div, {
btn_size: this.layout_numbers.vertical_zoom_area_width - padding,
vertical: true,
height: height,
init_val: model.getVertZoom(),
left: this.layout_numbers.canvas_left + width + padding,
top: this.layout_numbers.window_bar_height + padding,
onChange: function(val) {
vert_zoom_callback(val);
},
});
(function setUpZoomToFitButton() {
const btn_height =
self.layout_numbers.horizontal_zoom_area_height - padding;
const btn_width =
self.layout_numbers.vertical_zoom_area_width - padding;
const $btn = $('<div>')
.css({
position: 'absolute',
height: btn_height,
width: btn_width,
outline: 'solid 1px black',
left: self.layout_numbers.canvas_left + width + padding,
top: self.layout_numbers.canvas_top + height + padding,
cursor: 'pointer',
})
.addClass('oncoprint-zoomtofit-btn')
.appendTo($div);
$(`<img src="${zoomToFitIcon}" alt="Zoom to Fit Icon" />`)
.css({
height: btn_height - 4,
width: btn_width - 4,
'margin-top': -6,
'margin-left': 2,
})
.appendTo($btn);
$btn.hover(
function() {
$(this).css({ 'background-color': '#cccccc' });
},
function() {
$(this).css({ 'background-color': '#ffffff' });
}
);
zoom_to_fit_callback = zoom_to_fit_callback || function() {};
$btn.click(zoom_to_fit_callback);
})();
this.getWebGLContextAndSetUpMatrices();
this.setUpShaders();
this.overlay_ctx = $overlay_canvas[0].getContext('2d');
// Set up dragging
const resize_hit_zone = 5;
function mouseInRectDragZone(x: number, y: number) {
return (
x >= self.current_rect.left + resize_hit_zone &&
x <=
self.current_rect.left +
self.current_rect.width -
resize_hit_zone &&
y >= self.current_rect.top + resize_hit_zone &&
y <=
self.current_rect.top +
self.current_rect.height -
resize_hit_zone
);
}
function mouseInsideRectHitZone(x: number, y: number) {
return (
x >= self.current_rect.left - resize_hit_zone &&
x <=
self.current_rect.left +
self.current_rect.width +
resize_hit_zone &&
y >= self.current_rect.top - resize_hit_zone &&
y <=
self.current_rect.top +
self.current_rect.height +
resize_hit_zone
);
}
function mouseInRightHorzResizeZone(x: number, y: number) {
return (
!mouseInTopLeftResizeZone(x, y) &&
!mouseInTopRightResizeZone(x, y) &&
!mouseInBottomLeftResizeZone(x, y) &&
!mouseInBottomRightResizeZone(x, y) &&
mouseInsideRectHitZone(x, y) &&
Math.abs(
x - (self.current_rect.left + self.current_rect.width)
) < resize_hit_zone
);
}
function mouseInLeftHorzResizeZone(x: number, y: number) {
return (
!mouseInTopLeftResizeZone(x, y) &&
!mouseInTopRightResizeZone(x, y) &&
!mouseInBottomLeftResizeZone(x, y) &&
!mouseInBottomRightResizeZone(x, y) &&
mouseInsideRectHitZone(x, y) &&
Math.abs(x - self.current_rect.left) < resize_hit_zone
);
}
function mouseInTopVertResizeZone(x: number, y: number) {
return (
!mouseInTopLeftResizeZone(x, y) &&
!mouseInTopRightResizeZone(x, y) &&
!mouseInBottomLeftResizeZone(x, y) &&
!mouseInBottomRightResizeZone(x, y) &&
mouseInsideRectHitZone(x, y) &&
Math.abs(y - self.current_rect.top) < resize_hit_zone
);
}
function mouseInBottomVertResizeZone(x: number, y: number) {
return (
!mouseInTopLeftResizeZone(x, y) &&
!mouseInTopRightResizeZone(x, y) &&
!mouseInBottomLeftResizeZone(x, y) &&
!mouseInBottomRightResizeZone(x, y) &&
mouseInsideRectHitZone(x, y) &&
Math.abs(
y - (self.current_rect.top + self.current_rect.height)
) < resize_hit_zone
);
}
function mouseInTopLeftResizeZone(x: number, y: number) {
return (
Math.abs(y - self.current_rect.top) < resize_hit_zone &&
Math.abs(x - self.current_rect.left) < resize_hit_zone
);
}
function mouseInBottomLeftResizeZone(x: number, y: number) {
return (
Math.abs(
y - (self.current_rect.top + self.current_rect.height)
) < resize_hit_zone &&
Math.abs(x - self.current_rect.left) < resize_hit_zone
);
}
function mouseInTopRightResizeZone(x: number, y: number) {
return (
Math.abs(y - self.current_rect.top) < resize_hit_zone &&
Math.abs(
x - (self.current_rect.left + self.current_rect.width)
) < resize_hit_zone
);
}
function mouseInBottomRightResizeZone(x: number, y: number) {
return (
Math.abs(
y - (self.current_rect.top + self.current_rect.height)
) < resize_hit_zone &&
Math.abs(
x - (self.current_rect.left + self.current_rect.width)
) < resize_hit_zone
);
}
function updateRectResizeHoverLocation(x?: number, y?: number) {
if (typeof x === 'undefined') {
self.resize_hover = false;
} else {
if (mouseInRightHorzResizeZone(x, y)) {
self.resize_hover = 'r';
} else if (mouseInLeftHorzResizeZone(x, y)) {
self.resize_hover = 'l';
} else if (mouseInTopVertResizeZone(x, y)) {
self.resize_hover = 't';
} else if (mouseInBottomVertResizeZone(x, y)) {
self.resize_hover = 'b';
} else if (mouseInTopLeftResizeZone(x, y)) {
self.resize_hover = 'tl';
} else if (mouseInBottomRightResizeZone(x, y)) {
self.resize_hover = 'br';
} else if (mouseInBottomLeftResizeZone(x, y)) {
self.resize_hover = 'bl';
} else if (mouseInTopRightResizeZone(x, y)) {
self.resize_hover = 'tr';
} else {
self.resize_hover = false;
}
}
}
function updateCSSCursor(x?: number, y?: number) {
let cursor_val;
if (typeof x === 'undefined') {
cursor_val = 'auto';
} else {
if (mouseInRectDragZone(x, y)) {
cursor_val = 'move';
} else if (
mouseInRightHorzResizeZone(x, y) ||
mouseInLeftHorzResizeZone(x, y)
) {
cursor_val = 'ew-resize';
} else if (
mouseInTopVertResizeZone(x, y) ||
mouseInBottomVertResizeZone(x, y)
) {
cursor_val = 'ns-resize';
} else if (
mouseInTopLeftResizeZone(x, y) ||
mouseInBottomRightResizeZone(x, y)
) {
cursor_val = 'nwse-resize';
} else if (
mouseInBottomLeftResizeZone(x, y) ||
mouseInTopRightResizeZone(x, y)
) {
cursor_val = 'nesw-resize';
} else {
cursor_val = 'auto';
}
}
$div.css('cursor', cursor_val);
}
function getCanvasMouse(
view: OncoprintMinimapView,
div_mouse_x: number,
div_mouse_y: number
) {
const canv_top = parseInt(view.$canvas[0].style.top, 10);
const canv_left = parseInt(view.$canvas[0].style.left, 10);
const canv_width = parseInt(view.$canvas[0].width as any, 10);
const canv_height = parseInt(view.$canvas[0].height as any, 10);
const mouse_x = div_mouse_x - canv_left;
const mouse_y = div_mouse_y - canv_top;
const outside =
mouse_x < 0 ||
mouse_x >= canv_width ||
mouse_y < 0 ||
mouse_y >= canv_height;
return { mouse_x: mouse_x, mouse_y: mouse_y, outside: outside };
}
let dragging = false;
let drag_type:
| 'move'
| 'resize_r'
| 'resize_l'
| 'resize_b'
| 'resize_t'
| 'resize_tr'
| 'resize_br'
| 'resize_tl'
| 'resize_bl'
| false = false;
let drag_start_col = -1;
let drag_start_vert_scroll = -1;
let drag_start_x = -1;
let drag_start_y = -1;
let drag_start_vert_zoom = -1;
let y_ratio = -1;
let drag_start_rect: OverlayRectSpec;
$(document).on('mousedown', function(evt) {
const offset = self.$div.offset();
const overlay_mouse_x = evt.pageX - offset.left;
const overlay_mouse_y = evt.pageY - offset.top;
const mouse = getCanvasMouse(
self,
overlay_mouse_x,
overlay_mouse_y
);
if (!mouse.outside) {
const mouse_x = mouse.mouse_x;
const mouse_y = mouse.mouse_y;
dragging = false;
drag_type = false;
y_ratio =
model.getOncoprintHeight() /
parseInt(self.$canvas[0].height as any, 10);
if (mouseInRectDragZone(mouse_x, mouse_y)) {
drag_type = 'move';
} else if (mouseInRightHorzResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_r';
} else if (mouseInLeftHorzResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_l';
} else if (mouseInTopVertResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_t';
} else if (mouseInBottomVertResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_b';
} else if (mouseInTopRightResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_tr';
} else if (mouseInBottomRightResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_br';
} else if (mouseInTopLeftResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_tl';
} else if (mouseInBottomLeftResizeZone(mouse_x, mouse_y)) {
drag_type = 'resize_bl';
}
if (drag_type !== false) {
dragging = true;
drag_start_x = mouse_x;
drag_start_y = mouse_y;
drag_start_col = model.getClosestColumnIndexToLeft(
model.getHorzScroll(),
true
);
drag_start_vert_scroll = model.getVertScroll();
drag_start_vert_zoom = model.getVertZoom();
drag_start_rect = self.current_rect.clone();
}
}
});
$(document).on('mousemove', function(evt) {
const offset = self.$div.offset();
const overlay_mouse_x = evt.pageX - offset.left;
const overlay_mouse_y = evt.pageY - offset.top;
const mouse = getCanvasMouse(
self,
overlay_mouse_x,
overlay_mouse_y
);
const mouse_x = mouse.mouse_x;
const mouse_y = mouse.mouse_y;
let zoom = self.getZoom(model);
let cell_width = model.getCellWidth(true) * zoom.x;
if (dragging) {
evt.preventDefault();
let delta_col =
Math.floor(mouse_x / cell_width) -
Math.floor(drag_start_x / cell_width);
let delta_y = mouse_y - drag_start_y;
if (drag_type === 'move') {
const delta_y_scroll = delta_y * y_ratio;
drag_callback(
self.colToLeft(
model,
clamp(
drag_start_col + delta_col,
0,
model.getIdOrder().length - 1
)
),
drag_start_vert_scroll + delta_y_scroll
);
} else {
let render_rect: OverlayRectParams;
zoom = self.getZoom(model);
const max_num_cols = model.getIdOrder().length;
const min_num_cols = Math.ceil(
cell_view.getVisibleAreaWidth() /
(model.getCellWidth(true) +
model.getCellPadding(true, true))
);
const max_height = model.getOncoprintHeight(true) * zoom.y;
const min_height =
cell_view.getVisibleAreaHeight(model) * zoom.y;
const drag_start_right_col = drag_start_rect.right_col;
const drag_start_bottom =
drag_start_rect.top + drag_start_rect.height;
if (drag_type === 'resize_r') {
// Width must be valid
delta_col = clamp(
delta_col,
min_num_cols - drag_start_rect.num_cols,
max_num_cols - drag_start_rect.num_cols
);
// Right must be valid
delta_col = Math.min(
delta_col,
max_num_cols - drag_start_right_col
);
render_rect = {
top: drag_start_rect.top,
left_col: drag_start_rect.left_col,
right_col: drag_start_rect.right_col + delta_col,
height: drag_start_rect.height,
};
} else if (drag_type === 'resize_l') {
// Width must be valid
delta_col = clamp(
delta_col,
drag_start_rect.num_cols - max_num_cols,
drag_start_rect.num_cols - min_num_cols
);
// Left must be valid
delta_col = Math.max(
delta_col,
-drag_start_rect.left_col
);
render_rect = {
top: drag_start_rect.top,
left_col: drag_start_rect.left_col + delta_col,
right_col: drag_start_rect.right_col,
height: drag_start_rect.height,
};
} else if (drag_type === 'resize_t') {
// Height must be valid
delta_y = clamp(
delta_y,
drag_start_rect.height - max_height,
drag_start_rect.height - min_height
);
// Top must be valid
delta_y = Math.max(delta_y, -drag_start_rect.top);
render_rect = {
top: drag_start_rect.top + delta_y,
left_col: drag_start_rect.left_col,
right_col: drag_start_rect.right_col,
height: drag_start_rect.height - delta_y,
};
} else if (drag_type === 'resize_b') {
// Height must be valid
delta_y = clamp(
delta_y,
min_height - drag_start_rect.height,
max_height - drag_start_rect.height
);
// Bottom must be valid
delta_y = Math.min(
delta_y,
max_height - drag_start_bottom
);
render_rect = {
top: drag_start_rect.top,
left_col: drag_start_rect.left_col,
right_col: drag_start_rect.right_col,
height: drag_start_rect.height + delta_y,
};
} else if (drag_type === 'resize_tr') {
// Width must be valid
delta_col = clamp(
delta_col,
min_num_cols - drag_start_rect.num_cols,
max_num_cols - drag_start_rect.num_cols
);
// Right must be valid
delta_col = Math.min(
delta_col,
max_num_cols - drag_start_right_col
);
// Height must be valid
delta_y = clamp(
delta_y,
drag_start_rect.height - max_height,
drag_start_rect.height - min_height
);
// Top must be valid
delta_y = Math.max(delta_y, -drag_start_rect.top);
render_rect = {
top: drag_start_rect.top + delta_y,
left_col: drag_start_rect.left_col,
right_col: drag_start_rect.right_col + delta_col,
height: drag_start_rect.height - delta_y,
};
} else if (drag_type === 'resize_tl') {
// Width must be valid
delta_col = clamp(
delta_col,
drag_start_rect.num_cols - max_num_cols,
drag_start_rect.num_cols - min_num_cols
);
// Left must be valid
delta_col = Math.max(
delta_col,
-drag_start_rect.left_col
);
// Height must be valid
delta_y = clamp(
delta_y,
drag_start_rect.height - max_height,
drag_start_rect.height - min_height
);
// Top must be valid
delta_y = Math.max(delta_y, -drag_start_rect.top);
render_rect = {
top: drag_start_rect.top + delta_y,
left_col: drag_start_rect.left_col + delta_col,
right_col: drag_start_rect.right_col,
height: drag_start_rect.height - delta_y,
};
} else if (drag_type === 'resize_br') {
// Height must be valid
delta_y = clamp(
delta_y,
min_height - drag_start_rect.height,
max_height - drag_start_rect.height
);
// Bottom must be valid
delta_y = Math.min(
delta_y,
max_height - drag_start_bottom
);
// Width must be valid
delta_col = clamp(
delta_col,
min_num_cols - drag_start_rect.num_cols,
max_num_cols - drag_start_rect.num_cols
);
// Right must be valid
delta_col = Math.min(
delta_col,
max_num_cols - drag_start_right_col
);
render_rect = {
top: drag_start_rect.top,
left_col: drag_start_rect.left_col,
right_col: drag_start_rect.right_col + delta_col,
height: drag_start_rect.height + delta_y,
};
} else if (drag_type === 'resize_bl') {
// Height must be valid
delta_y = clamp(
delta_y,
min_height - drag_start_rect.height,
max_height - drag_start_rect.height
);
// Bottom must be valid
delta_y = Math.min(
delta_y,
max_height - drag_start_bottom
);
// Width must be valid
delta_col = clamp(
delta_col,
drag_start_rect.num_cols - max_num_cols,
drag_start_rect.num_cols - min_num_cols
);
// Left must be valid
delta_col = Math.max(
delta_col,
-drag_start_rect.left_col
);
render_rect = {
top: drag_start_rect.top,
left_col: drag_start_rect.left_col + delta_col,
right_col: drag_start_rect.right_col,
height: drag_start_rect.height + delta_y,
};
}
self.current_rect.setParams(render_rect);
self.drawOverlayRect(null, null, self.current_rect);
}
} else {
if (mouse.outside) {
updateCSSCursor();
updateRectResizeHoverLocation();
} else {
updateCSSCursor(mouse_x, mouse_y);
updateRectResizeHoverLocation(mouse_x, mouse_y);
}
self.drawOverlayRect(model, cell_view);
}
});
function endDrag() {
if (dragging) {
if (
[
'resize_t',
'resize_b',
'resize_l',
'resize_r',
'resize_tl',
'resize_tr',
'resize_bl',
'resize_br',
].indexOf(drag_type as any) > -1
) {
viewport_callback({
left_col: self.current_rect.left_col,
scroll_y_proportion:
self.current_rect.top /
parseInt(self.$canvas[0].height as any, 10),
right_col: self.current_rect.right_col,
zoom_y:
(drag_start_rect.height /
self.current_rect.height) *
drag_start_vert_zoom,
});
}
dragging = false;
drag_type = false;
}
}
$(document).on('mouseup', function(evt) {
const offset = self.$div.offset();
const overlay_mouse_x = evt.pageX - offset.left;
const overlay_mouse_y = evt.pageY - offset.top;
endDrag();
const mouse = getCanvasMouse(
self,
overlay_mouse_x,
overlay_mouse_y
);
if (!mouse.outside) {
let mouse_x = mouse.mouse_x;
let mouse_y = mouse.mouse_y;
updateCSSCursor(mouse_x, mouse_y);
updateRectResizeHoverLocation(mouse_x, mouse_y);
} else {
updateCSSCursor();
updateRectResizeHoverLocation();
}
self.drawOverlayRect(model, cell_view);
});
(function setUpWindowDrag() {
let start_mouse_x = 0;
let start_mouse_y = 0;
let start_left = 0;
let start_top = 0;
function handleDrag(evt: MouseMoveEvent) {
evt.preventDefault();
const delta_mouse_x = evt.pageX - start_mouse_x;
const delta_mouse_y = evt.pageY - start_mouse_y;
self.setWindowPosition(
start_left + delta_mouse_x,
start_top + delta_mouse_y
);
}
self.$window_bar.hover(
function() {
$(this).css({ cursor: 'move' });
},
function() {
$(this).css({ cursor: 'auto' });
}
);
self.$window_bar.on('mousedown', function(evt) {
start_mouse_x = evt.pageX;
start_mouse_y = evt.pageY;
start_left = parseInt(self.$div.css('left'), 10);
start_top = parseInt(self.$div.css('top'), 10);
$(document).on('mousemove', handleDrag);
});
$(document).on('mouseup click', function() {
$(document).off('mousemove', handleDrag);
});
})();
}
private colToLeft(model: OncoprintModel, colIndex: number) {
return model.getZoomedColumnLeft(model.getIdOrder()[colIndex]);
}
private get shouldRender() {
return this.visible && !this.rendering_suppressed;
}
private getNewCanvas() {
const old_canvas = this.$canvas[0];
old_canvas.removeEventListener(
'webglcontextlost',
this.handleContextLost
);
const new_canvas = old_canvas.cloneNode();
new_canvas.addEventListener('webglcontextlost', this.handleContextLost);
const parent_node = old_canvas.parentNode;
parent_node.removeChild(old_canvas);
parent_node.insertBefore(new_canvas, this.$overlay_canvas[0]);
this.$canvas = $(new_canvas) as JQuery<HTMLCanvasElement>;
this.ctx = null;
this.ext = null;
}
private getWebGLCanvasContext() {
try {
const canvas = this.$canvas[0];
const ctx =
this.ctx ||
(canvas.getContext('experimental-webgl', {
alpha: false,
antialias: true,
}) as OncoprintWebGLContext);
ctx.clearColor(1.0, 1.0, 1.0, 1.0);
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
ctx.viewportWidth = canvas.width;
ctx.viewportHeight = canvas.height;
ctx.viewport(0, 0, ctx.viewportWidth, ctx.viewportHeight);
ctx.enable(ctx.DEPTH_TEST);
ctx.enable(ctx.BLEND);
ctx.blendEquation(ctx.FUNC_ADD);
ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
ctx.depthMask(false);
return ctx;
} catch (e) {
return null;
}
}
private ensureWebGLContext() {
for (let i = 0; i < 5; i++) {
if (!this.ctx || this.ctx.isContextLost()) {
// have to get a new canvas when context is lost by browser
this.getNewCanvas();
this.ctx = this.getWebGLCanvasContext();
this.setUpShaders();
} else {
break;
}
}
if (!this.ctx || this.ctx.isContextLost()) {
throw new Error(
'Unable to get WebGL context for Oncoprint Minimap'
);
}
}
private createShaderProgram(
vertex_shader: WebGLShader,
fragment_shader: WebGLShader
) {
const program = this.ctx.createProgram();
this.ctx.attachShader(program, vertex_shader);
this.ctx.attachShader(program, fragment_shader);
this.ctx.linkProgram(program);
const success = this.ctx.getProgramParameter(
program,
this.ctx.LINK_STATUS
);
if (!success) {
const msg = this.ctx.getProgramInfoLog(program);
this.ctx.deleteProgram(program);
throw 'Unable to link shader program: ' + msg;
}
return program;
}
private createShader(
source: string,
type: 'VERTEX_SHADER' | 'FRAGMENT_SHADER'
) {
const shader = this.ctx.createShader(this.ctx[type]);
this.ctx.shaderSource(shader, source);
this.ctx.compileShader(shader);
const success = this.ctx.getShaderParameter(
shader,
this.ctx.COMPILE_STATUS
);
if (!success) {
const msg = this.ctx.getShaderInfoLog(shader);
this.ctx.deleteShader(shader);
throw 'Unable to compile shader: ' + msg;
}
return shader;
}
private getWebGLContextAndSetUpMatrices() {
this.ctx = this.getWebGLCanvasContext();
if (this.ctx) {
this.ext = this.ctx.getExtension('ANGLE_instanced_arrays');
}
(function initializeMatrices(self) {
const mvMatrix = gl_matrix.mat4.create();
gl_matrix.mat4.lookAt(mvMatrix, [0, 0, 1], [0, 0, 0], [0, 1, 0]);
self.mvMatrix = mvMatrix;
const pMatrix = gl_matrix.mat4.create();
gl_matrix.mat4.ortho(
pMatrix,
0,
self.ctx.viewportWidth,
self.ctx.viewportHeight,
0,
-5,
1000
); // y axis inverted so that y increases down like SVG
self.pMatrix = pMatrix;
})(this);
}
private setUpShaders() {
const vertex_shader_source = [
'precision highp float;',
'attribute float aPosVertex;',
'attribute float aColVertex;',
'attribute float aVertexOncoprintColumn;',
'uniform float columnWidth;',
'uniform float zoomX;',
'uniform float zoomY;',
'uniform mat4 uMVMatrix;',
'uniform mat4 uPMatrix;',
'uniform float offsetY;',
'uniform float positionBitPackBase;',
'uniform float texSize;',
'varying float texCoord;',
'vec3 unpackVec3(float packedVec3, float base) {',
' float pos0 = floor(packedVec3 / (base*base));',
' float pos0Contr = pos0*base*base;',
' float pos1 = floor((packedVec3 - pos0Contr)/base);',
' float pos1Contr = pos1*base;',
' float pos2 = packedVec3 - pos0Contr - pos1Contr;',
' return vec3(pos0, pos1, pos2);',
'}',
'void main(void) {',
' gl_Position = vec4(unpackVec3(aPosVertex, positionBitPackBase), 1.0);',
' gl_Position[0] += aVertexOncoprintColumn*columnWidth;',
' gl_Position[1] += offsetY;',
' gl_Position *= vec4(zoomX, zoomY, 1.0, 1.0);',
' gl_Position = uPMatrix * uMVMatrix * gl_Position;',
' texCoord = (aColVertex + 0.5) / texSize;',
'}',
].join('\n');
const fragment_shader_source = [
'precision mediump float;',
'varying float texCoord;',
'uniform sampler2D uSampler;',
'void main(void) {',
' gl_FragColor = texture2D(uSampler, vec2(texCoord, 0.5));',
'}',
].join('\n');
const vertex_shader = this.createShader(
vertex_shader_source,
'VERTEX_SHADER'
);
const fragment_shader = this.createShader(
fragment_shader_source,
'FRAGMENT_SHADER'
);
const shader_program = this.createShaderProgram(
vertex_shader,
fragment_shader
) as OncoprintShaderProgram;
shader_program.vertexPositionAttribute = this.ctx.getAttribLocation(
shader_program,
'aPosVertex'
);
this.ctx.enableVertexAttribArray(
shader_program.vertexPositionAttribute
);
shader_program.vertexColorAttribute = this.ctx.getAttribLocation(
shader_program,
'aColVertex'
);
this.ctx.enableVertexAttribArray(shader_program.vertexColorAttribute);
shader_program.vertexOncoprintColumnAttribute = this.ctx.getAttribLocation(
shader_program,
'aVertexOncoprintColumn'
);
this.ctx.enableVertexAttribArray(
shader_program.vertexOncoprintColumnAttribute
);
shader_program.samplerUniform = this.ctx.getUniformLocation(
shader_program,
'uSampler'
);
shader_program.pMatrixUniform = this.ctx.getUniformLocation(
shader_program,
'uPMatrix'
);
shader_program.mvMatrixUniform = this.ctx.getUniformLocation(
shader_program,
'uMVMatrix'
);
shader_program.columnWidthUniform = this.ctx.getUniformLocation(
shader_program,
'columnWidth'
);
shader_program.zoomXUniform = this.ctx.getUniformLocation(
shader_program,
'zoomX'
);
shader_program.zoomYUniform = this.ctx.getUniformLocation(
shader_program,
'zoomY'
);
shader_program.offsetYUniform = this.ctx.getUniformLocation(
shader_program,
'offsetY'
);
shader_program.positionBitPackBaseUniform = this.ctx.getUniformLocation(
shader_program,
'positionBitPackBase'
);
shader_program.texSizeUniform = this.ctx.getUniformLocation(
shader_program,
'texSize'
);
this.shader_program = shader_program;
}
private getTrackBuffers(
cell_view: OncoprintWebGLCellView,
track_id: TrackId
) {
const pos_buffer = this.ctx.createBuffer() as OncoprintVertexTrackBuffer;
const pos_array = cell_view.vertex_data[track_id].pos_array;
const universal_shapes_start_index =
cell_view.vertex_data[track_id].universal_shapes_start_index;
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, pos_buffer);
this.ctx.bufferData(
this.ctx.ARRAY_BUFFER,
pos_array,
this.ctx.STATIC_DRAW
);
pos_buffer.itemSize = 1;
pos_buffer.specificShapesNumItems =
universal_shapes_start_index / pos_buffer.itemSize;
pos_buffer.universalShapesNumItems =
(pos_array.length - universal_shapes_start_index) /
pos_buffer.itemSize;
const col_buffer = this.ctx.createBuffer() as OncoprintVertexTrackBuffer;
const col_array = cell_view.vertex_data[track_id].col_array;
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, col_buffer);
this.ctx.bufferData(
this.ctx.ARRAY_BUFFER,
col_array,
this.ctx.STATIC_DRAW
);
col_buffer.itemSize = 1;
col_buffer.specificShapesNumItems =
universal_shapes_start_index / col_buffer.itemSize;
col_buffer.universalShapesNumItems =
(col_array.length - universal_shapes_start_index) /
col_buffer.itemSize;
const tex = this.ctx.createTexture();
this.ctx.bindTexture(this.ctx.TEXTURE_2D, tex);
const color_bank = cell_view.vertex_data[track_id].col_bank;
const width = Math.pow(
2,
Math.ceil((Math as any).log2(color_bank.length / 4))
);
while (color_bank.length < 4 * width) {
color_bank.push(0);
}
const height = 1;
this.ctx.texImage2D(
this.ctx.TEXTURE_2D,
0,
this.ctx.RGBA,
width,
height,
0,
this.ctx.RGBA,
this.ctx.UNSIGNED_BYTE,
new Uint8Array(color_bank)
);
this.ctx.texParameteri(
this.ctx.TEXTURE_2D,
this.ctx.TEXTURE_MIN_FILTER,
this.ctx.NEAREST
);
this.ctx.texParameteri(
this.ctx.TEXTURE_2D,
this.ctx.TEXTURE_MAG_FILTER,
this.ctx.NEAREST
);
const color_texture = { texture: tex, size: width };
const vertex_column_buffer = this.ctx.createBuffer() as OncoprintTrackBuffer;
const vertex_column_array = cell_view.vertex_column_array[track_id];
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, vertex_column_buffer);
this.ctx.bufferData(
this.ctx.ARRAY_BUFFER,
new Float32Array(vertex_column_array),
this.ctx.STATIC_DRAW
);
vertex_column_buffer.itemSize = 1;
vertex_column_buffer.numItems =
vertex_column_array.length / vertex_column_buffer.itemSize;
return {
position: pos_buffer,
color: col_buffer,
color_tex: color_texture,
column: vertex_column_buffer,
};
}
private getSimpleCountBuffer(model: OncoprintModel) {
const numColumns = model.getIdOrder().length;
const buffer = this.ctx.createBuffer() as OncoprintTrackBuffer;
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffer);
this.ctx.bufferData(
this.ctx.ARRAY_BUFFER,
new Float32Array(_.range(0, numColumns)),
this.ctx.STATIC_DRAW
);
buffer.itemSize = 1;
buffer.numItems = numColumns;
return buffer;
}
private drawOncoprint(
model: OncoprintModel,
cell_view: OncoprintWebGLCellView
) {
if (!this.shouldRender) {
return;
}
this.ensureWebGLContext();
const zoom = this.getZoom(model);
this.ctx.clearColor(1.0, 1.0, 1.0, 1.0);
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT | this.ctx.DEPTH_BUFFER_BIT);
const tracks = model.getTracks();
const simple_count_buffer = this.getSimpleCountBuffer(model);
for (let i = 0; i < tracks.length; i++) {
const track_id = tracks[i];
const cell_top = model.getCellTops(track_id, true);
const buffers = this.getTrackBuffers(cell_view, track_id);
if (buffers.position.numItems === 0) {
continue;
}
for (const forSpecificShapes of [false, true]) {
const shader_program = this.shader_program;
this.ctx.useProgram(shader_program);
if (forSpecificShapes) {
this.ctx.bindBuffer(
this.ctx.ARRAY_BUFFER,
buffers.position
);
this.ctx.vertexAttribPointer(
shader_program.vertexPositionAttribute,
buffers.position.itemSize,
this.ctx.FLOAT,
false,
0,
0
);
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color);
this.ctx.vertexAttribPointer(
shader_program.vertexColorAttribute,
buffers.color.itemSize,
this.ctx.FLOAT,
false,
0,
0
);
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.column);
this.ctx.vertexAttribPointer(
shader_program.vertexOncoprintColumnAttribute,
buffers.column.itemSize,
this.ctx.FLOAT,
false,
0,
0
);
// make sure to set divisor 0, otherwise the track will only use the first item in the column buffer
this.ext.vertexAttribDivisorANGLE(
shader_program.vertexOncoprintColumnAttribute,
0
);
} else {
// set up for drawArraysInstanced
const universalShapesStart =
buffers.position.specificShapesNumItems *
buffers.po