client/client/modules/render/tracks/ruler/rulerBrush.js (426 lines of code) (raw):
import * as PIXI from 'pixi.js-legacy';
import {PixiTextSize} from '../../utilities';
import {drawingConfiguration} from '../../core';
import getRulerHeight from './rulerHeightManager';
const Math = window.Math;
export default class RulerBrush {
viewport;
drag = new Map();
brush = null;
brushArea = null;
pickLocationArea = null;
startCursor = null;
endCursor = null;
selectableArea = null;
selectionRegion = null;
selectionRegionWidthLabel = null;
_globalRulerOffsetY = 0;
_localRulerOffsetY = 0;
_cursorYOffset = 0;
_localRulerHeight = 0;
_globalRulerHeight = 0;
_eventListenersDestructors = [];
constructor(callback, viewport, config) {
this._config = config;
this.viewport = viewport;
this._globalRulerHeight = getRulerHeight(this._config.global);
this._localRulerHeight = getRulerHeight(this._config.local);
this._globalRulerOffsetY = this._config.brush.line.thickness;
this._localRulerOffsetY = this._globalRulerOffsetY +
this._globalRulerHeight +
this._config.rulersVerticalMargin +
this._config.brush.line.thickness;
this._cursorYOffset = this._globalRulerOffsetY - this._config.brush.line.thickness;
this.moveBrush = callback;
this.container = new PIXI.Container();
this.brush = new PIXI.Graphics();
this.brushArea = this._createInteractiveElement();
this.shortenedBrush = new PIXI.Graphics();
this.pickLocationArea = this.createPickLocationArea();
this.startCursor = this.createCursor();
this.endCursor = this.createCursor();
this.selectableArea = this.createSelectableArea();
this.selectionRegion = new PIXI.Graphics();
this.selectionRegionWidthLabel = new PIXI.Text('', this._config.brush.drag.label);
this.selectionRegionWidthLabel.resolution = drawingConfiguration.resolution;
this.drag.set(this.startCursor, null);
this.drag.set(this.endCursor, null);
this.drag.set(this.brushArea, null);
this.drag.set(this.selectableArea, null);
this.drag.set(this.pickLocationArea, null);
this.container.addChild(this.pickLocationArea);
this.container.addChild(this.brushArea);
this.container.addChild(this.brush);
this.container.addChild(this.shortenedBrush);
this.container.addChild(this.startCursor);
this.container.addChild(this.endCursor);
this.container.addChild(this.selectableArea);
this.container.addChild(this.selectionRegion);
this.container.addChild(this.selectionRegionWidthLabel);
}
clearData() {
for (let i = 0; i < this._eventListenersDestructors.length; i++) {
this._eventListenersDestructors[i]();
}
this.drag = null;
this._eventListenersDestructors = null;
}
render() {
this.changeArea();
this.changeBrush();
this.changeShortenedBrush();
this.changeCursor(this.startCursor, this.viewport.brush.start, this.viewport.brush.end);
this.changeCursor(this.endCursor, this.viewport.brush.end, this.viewport.brush.start);
this._drawSelection(this.viewport.brush, false, true, true);
}
changeBrush() {
this.brush.clear();
const thickness = this._config.brush.line.thickness;
const halfThickness = thickness / 2;
const normalize = (val) => Math.max(halfThickness, Math.min(this.viewport.canvasSize - halfThickness, val));
this.brush
.lineStyle(thickness, this.viewport.isShortenedIntronsMode ? this._config.brushColor.shortenedIntrons : this._config.brushColor.normal, 1)
.moveTo(0, this._localRulerOffsetY)
.lineTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.start) + halfThickness),
this._localRulerOffsetY)
.moveTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.start)), this._localRulerOffsetY)
.lineTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.start)),
this._globalRulerOffsetY - thickness)
.moveTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.end)),
this._globalRulerOffsetY - thickness)
.lineTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.end)), this._localRulerOffsetY)
.moveTo(normalize(this.viewport.project.chromoBP2pixel(this.viewport.brush.end) - halfThickness),
this._localRulerOffsetY)
.lineTo(this.viewport.canvasSize, this._localRulerOffsetY);
}
changeShortenedBrush() {
this.shortenedBrush.clear();
if (this.viewport.isShortenedIntronsMode) {
const thickness = 1;
const halfThickness = thickness / 2;
for (let i = 0; i < this.viewport.shortenedIntronsViewport._coveredRange.ranges.length; i++) {
const range = this.viewport.shortenedIntronsViewport._coveredRange.ranges[i];
const x = this.viewport.project.brushBP2pixel(range.startIndex) - this.viewport.factor / 2;
if (this.viewport.shortenedIntronsViewport.intronLength) {
this.shortenedBrush
.lineStyle(0, this._config.brushColor.shortenedIntrons, 0)
.beginFill(this._config.brushColor.shortenedIntrons, this._config.brushColor.shortenedIntronsAlpha)
.drawRect(
Math.floor(x - this.viewport.shortenedIntronsViewport.intronLength * this.viewport.factor),
this._localRulerOffsetY,
2 * this.viewport.shortenedIntronsViewport.intronLength * this.viewport.factor,
this._config.local.body.height
)
.endFill();
}
this.shortenedBrush
.lineStyle(thickness, this._config.brushColor.shortenedIntrons, 1)
.moveTo(Math.floor(x) - halfThickness, this._localRulerOffsetY)
.lineTo(Math.floor(x) - halfThickness, this._localRulerOffsetY + this._config.local.body.height);
}
}
}
createSelectableArea() {
const area = this._createInteractiveElement(false);
const white = 0xFFFFFF;
area.beginFill(white, 1)
.drawRect(0, this._localRulerOffsetY, this.viewport.canvasSize, this._localRulerHeight);
area.alpha = 0;
return area;
}
createPickLocationArea() {
const area = this._createInteractiveElement(false);
const white = 0xFFFFFF;
area.beginFill(white, 1)
.drawRect(0, this._globalRulerOffsetY, this.viewport.canvasSize, this._globalRulerHeight);
area.alpha = 0;
return area;
}
createCursor() {
const cursor = this._createInteractiveElement();
cursor.y = this._cursorYOffset;
return cursor;
}
_createInteractiveElement(buttonMode = true) {
const element = new PIXI.Graphics();
element.interactive = true;
element.interactiveChildren = false;
element.buttonMode = buttonMode;
const _onDragStart = ::this.onDragStart;
const _onDragEnd = ::this.onDragEnd;
const _onDragMove = ::this.onDragMove;
element
.on('mousedown', _onDragStart)
.on('touchstart', _onDragStart)
.on('mouseup', _onDragEnd)
.on('mouseupoutside', _onDragEnd)
.on('touchend', _onDragEnd)
.on('touchendoutside', _onDragEnd)
.on('mousemove', _onDragMove)
.on('touchmove', _onDragMove);
const eventListenersDestructor = function() {
element
.off('mousedown', _onDragStart)
.off('touchstart', _onDragStart)
.off('mouseup', _onDragEnd)
.off('mouseupoutside', _onDragEnd)
.off('touchend', _onDragEnd)
.off('touchendoutside', _onDragEnd)
.off('mousemove', _onDragMove)
.off('touchmove', _onDragMove);
};
this._eventListenersDestructors.push(eventListenersDestructor);
return element;
}
changeArea() {
this.brushArea.clear();
const areaPosition = {
x: this.viewport.project.chromoBP2pixel(this.viewport.brush.start),
y: this._globalRulerOffsetY
};
const areaSize = {
height: this._config.global.body.height,
width: this.viewport.convert.chromoBP2pixel(this.viewport.brushSize)
};
this.brushArea
.beginFill(this._config.brush.area.color, this._config.brush.area.alpha)
.drawRect(areaPosition.x, areaPosition.y, areaSize.width, areaSize.height)
.endFill();
const thresholdFactor = 2;
const notchWidthThreshold = thresholdFactor * this._config.brush.drag.notch.width;
if (areaSize.width > notchWidthThreshold) {
this.drawDragNotches(this.brushArea, areaPosition, areaSize);
}
}
changeCursor(cursor, position, oppositePosition) {
cursor.clear();
let canvasPosition = this.viewport.project.chromoBP2pixel(position);
const canvasOppositePosition = this.viewport.project.chromoBP2pixel(oppositePosition);
const distance = Math.abs(canvasOppositePosition - canvasPosition);
if (distance < this._config.brush.cursor.width) {
const center = (canvasPosition + canvasOppositePosition) / 2;
if (oppositePosition >= position) {
canvasPosition = center - this._config.brush.cursor.width / 2;
}
else {
canvasPosition = center + this._config.brush.cursor.width / 2;
}
}
cursor.x = Math.min(Math.max(canvasPosition - this._config.brush.cursor.width / 2, 0),
this.viewport.canvasSize - this._config.brush.cursor.width);
cursor
.beginFill(this.viewport.isShortenedIntronsMode ? this._config.brushColor.shortenedIntrons : this._config.brushColor.normal, 1)
.drawRect(0, 0, this._config.brush.cursor.width, this._config.brush.cursor.height)
.endFill();
this.drawNotches(cursor);
}
drawNotches(cursor) {
const thickness = this._config.brush.cursor.notch.thickness;
const notchMargin = Math.floor(
(this._config.brush.cursor.width - this._config.brush.cursor.notch.count * thickness) /
this._config.brush.cursor.notch.count
);
const y = (this._config.brush.cursor.height - this._config.brush.cursor.notch.height) / 2;
const firstNotchPosition = this._config.brush.cursor.width / 2 -
(this._config.brush.cursor.notch.count - 1) / 2 * (notchMargin + thickness) - thickness / 2;
for (let notch = 0; notch < this._config.brush.cursor.notch.count; notch++) {
cursor
.lineStyle(thickness, this._config.brush.cursor.notch.color, 1)
.moveTo(firstNotchPosition + notch * (notchMargin + thickness), y)
.lineTo(firstNotchPosition + notch * (notchMargin + thickness),
y + this._config.brush.cursor.notch.height);
}
}
drawDragNotches(area, areaPosition, areaSize) {
const thickness = this._config.brush.drag.notch.thickness;
const dY = (areaSize.height - this._config.brush.drag.height) / 2;
const notchMargin = Math.floor(
(this._config.brush.drag.height - this._config.brush.drag.notches * thickness) /
this._config.brush.drag.notches
);
const x = areaPosition.x + (areaSize.width - this._config.brush.drag.notch.width) / 2;
const firstNotchPosition = areaPosition.y + dY + this._config.brush.drag.height / 2 -
(this._config.brush.drag.notches - 1) / 2 * (notchMargin + thickness) - thickness / 2;
for (let notch = 0; notch < this._config.brush.drag.notches; notch++) {
area
.lineStyle(thickness, this._config.brush.drag.notch.color, 1)
.moveTo(x, firstNotchPosition + notch * (notchMargin + thickness))
.lineTo(x + this._config.brush.drag.notch.width,
firstNotchPosition + notch * (notchMargin + thickness));
}
}
createNewBrush(target, dragData, dx) {
if (target === this.startCursor) {
return {start: Math.min(dragData.brush.start + dx, dragData.brush.end)};
}
else if (target === this.endCursor) {
return {end: Math.max(dragData.brush.end + dx, dragData.brush.start)};
}
else if (target === this.brushArea || target === this.pickLocationArea) {
return {delta: dx};
}
}
onDragStart(event) {
this.drag.set(event.currentTarget, {
brush: this.viewport.brush,
point: event.data.global.clone()
});
this._drawSelection(null);
if (this.updateScene) {
this.updateScene();
}
}
onDragEnd(event) {
const dragData = this.drag.get(event.currentTarget);
if (dragData) {
const oldPoint = Math.round(this.viewport.project.pixel2chromoBP(this.container.toLocal(dragData.point).x));
const newPoint = Math.round(this.viewport.project.pixel2chromoBP(this.container.toLocal(event.data.global).x));
const wasDragging = oldPoint !== newPoint;
if (event.currentTarget === this.selectableArea && wasDragging) {
const region = this._getSelectionRegion(event);
this._doSelection(region);
}
else if (event.currentTarget === this.pickLocationArea && !wasDragging) {
if (oldPoint < this.viewport.brush.start || oldPoint > this.viewport.brush.end) {
const delta = Math.round(oldPoint - (this.viewport.brush.start + this.viewport.brush.end) / 2);
this.moveBrush(this.createNewBrush(event.currentTarget, dragData, delta));
}
}
else if (event.currentTarget === this.startCursor || event.currentTarget === this.endCursor || event.currentTarget === this.brushArea) {
const dx = newPoint - oldPoint;
this.moveBrush(this.createNewBrush(event.currentTarget, dragData, dx));
}
}
this.drag.set(event.currentTarget, null);
this._drawSelection(this.viewport.brush, false, true, true);
if (this.updateScene) {
this.updateScene();
}
}
onDragMove(event) {
const dragData = this.drag.get(event.currentTarget);
if (dragData !== null) {
const target = event.currentTarget;
if (target === this.selectableArea) {
const region = this._getSelectionRegion(event);
if (region) {
this._drawSelection(region, true, false);
}
}
else if (target === this.startCursor || target === this.endCursor || target === this.brushArea) {
const oldPoint = this.container.toLocal(dragData.point);
const newPoint = this.container.toLocal(event.data.global);
const dx = this.viewport.convert.pixel2chromoBP(newPoint.x - oldPoint.x);
this.moveBrush(this.createNewBrush(target, dragData, dx));
if (event.currentTarget === this.brushArea) {
this.drag.set(event.currentTarget, {
brush: this.viewport.brush,
point: event.data.global.clone()
});
}
}
if (this.updateScene) {
this.updateScene();
}
}
}
_getSelectionRegion(event) {
const dragData = this.drag.get(event.currentTarget);
if (dragData !== null) {
const oldPoint = Math.max(0, Math.min(this.container.toLocal(dragData.point).x, this.viewport.canvasSize));
const newPoint = Math.max(0, Math.min(this.container.toLocal(event.data.global).x, this.viewport.canvasSize));
return {
end: this.viewport.project.pixel2brushBP(Math.max(oldPoint, newPoint)),
start: this.viewport.project.pixel2brushBP(Math.min(oldPoint, newPoint))
};
}
return null;
}
_drawSelection(selection, drawRegion = false, displayFractionDigits = true, isTotalRegion = false) {
this.selectionRegion.clear();
if (selection) {
const position = {
x: isTotalRegion ? 0 : this.viewport.project.brushBP2pixel(selection.start),
y: this._localRulerOffsetY - this._config.brush.line.thickness
};
const size = {
height: this._localRulerHeight + this._config.brush.line.thickness,
width: isTotalRegion ? this.viewport.canvasSize : this.viewport.project
.brushBP2pixel(selection.end) -
this.viewport.project
.brushBP2pixel(selection.start)
};
if (drawRegion) {
this.selectionRegion
.beginFill(
this.viewport.isShortenedIntronsMode ?
this._config.brushColor.shortenedIntrons :
this._config.brushColor.normal,
this._config.brush.area.selection.alpha
)
.drawRect(position.x, position.y, size.width, size.height);
}
if (!isTotalRegion) {
const dashSize = 2;
const dashes = size.height / (2 * dashSize - 1);
const thickness = 1;
this.selectionRegion
.lineStyle(
thickness,
this.viewport.isShortenedIntronsMode ?
this._config.brushColor.shortenedIntrons :
this._config.brushColor.normal,
1
);
for (let i = 0; i < dashes; i++) {
this.selectionRegion
.moveTo(position.x + thickness / 2, position.y + 2 * i * dashSize)
.lineTo(position.x + thickness / 2, position.y + (2 * i + 1) * dashSize)
.moveTo(position.x + size.width - thickness / 2, position.y + 2 * i * dashSize)
.lineTo(position.x + size.width - thickness / 2, position.y + (2 * i + 1) * dashSize);
}
}
const regionWidthText = this._getSelectionDescription(selection, isTotalRegion, displayFractionDigits);
const labelSize = PixiTextSize.getTextSize(regionWidthText, this._config.brush.drag.label);
const shouldDisplayLabel = labelSize.width + 2 * this._config.brush.drag.region.arrow.width < size.width;
if (shouldDisplayLabel) {
this.selectionRegionWidthLabel.text = regionWidthText;
this.selectionRegionWidthLabel.x = Math.round(position.x + size.width / 2 - labelSize.width / 2);
this.selectionRegionWidthLabel.y = Math.round(this._localRulerOffsetY + this._config.local.body.height / 2 - labelSize.height / 2);
}
else {
this.selectionRegionWidthLabel.text = '';
}
this._drawSelectionRegionArrows(position, size, shouldDisplayLabel);
}
else {
this.selectionRegionWidthLabel.text = '';
}
}
_getSelectionDescription(selection, isTotalRegion, displayFractionDigits) {
let selectionDescription = '';
if (this.viewport.shortenedIntronsViewport.shortenedIntronsMode) {
const realWidth = `${this._config.brush.drag.formatter(
Math.floor(selection.end - selection.start + (isTotalRegion ? 1 : 0)),
displayFractionDigits
)}bp`;
const shortenedWidth = `${this._config.brush.drag.formatter(
Math.floor(this.viewport.shortenedIntronsViewport
.getShortenedSize({end: selection.end, start: selection.start}) + (isTotalRegion ? 1 : 0)),
displayFractionDigits
)}bp`;
selectionDescription = `${shortenedWidth} (${realWidth})`;
}
else {
selectionDescription = `${this._config.brush.drag.formatter(
Math.floor(selection.end - selection.start + (isTotalRegion ? 1 : 0)),
displayFractionDigits
)}bp`;
}
return selectionDescription;
}
_drawSelectionRegionArrows(position, size, labelIsVisible = false) {
const dashSize = 4;
const dashes = (size.width - 2 * dashSize) / (dashSize * 2);
const line = this._localRulerOffsetY + this._config.local.body.height / 2;
const arrowSize = this._config.brush.drag.region.arrow.width * Math.sqrt(2) / 2; // 45`
this.selectionRegion
.lineStyle(this._config.brush.drag.region.line.thickness, this._config.brush.drag.region.line.color, 1);
for (let i = 0; i < dashes; i++) {
const xStart = position.x + dashSize + 2 * i * dashSize;
const xEnd = position.x + dashSize + (2 * i + 1) * dashSize;
if (labelIsVisible) {
if ((xStart > this.selectionRegionWidthLabel.x && xStart < this.selectionRegionWidthLabel.x + this.selectionRegionWidthLabel.width)
||
(xEnd > this.selectionRegionWidthLabel.x && xEnd < this.selectionRegionWidthLabel.x + this.selectionRegionWidthLabel.width)) {
continue;
}
}
this.selectionRegion
.moveTo(xStart, line)
.lineTo(xEnd, line);
}
const arrowMargin = this._config.brush.drag.region.arrow.margin;
if (size.width > 2 * (arrowSize + arrowMargin)) {
this.selectionRegion
.moveTo(position.x + arrowMargin, line)
.lineTo(position.x + arrowMargin + arrowSize, line - arrowSize)
.moveTo(position.x + arrowMargin, line)
.lineTo(position.x + arrowMargin + arrowSize, line + arrowSize)
.moveTo(position.x + size.width - arrowMargin, line)
.lineTo(position.x + size.width - arrowMargin - arrowSize, line - arrowSize)
.moveTo(position.x + size.width - arrowMargin, line)
.lineTo(position.x + size.width - arrowMargin - arrowSize, line + arrowSize);
}
}
_doSelection(selection) {
if (selection) {
this.moveBrush(selection);
}
}
}