import {ListenerHandler} from '../utils/listen';
import {DrawController} from '../app/drawController';
import {getScaledOffset} from './layerGroup';
import {InteractionEventNames} from './generic';
// external
import Konva from 'konva';
// doc imports
/* eslint-disable no-unused-vars */
import {Point, Point3D} from '../math/point';
import {Index} from '../math/index';
import {Vector3D} from '../math/vector';
import {Scalar2D, Scalar3D} from '../math/scalar';
import {PlaneHelper} from '../image/planeHelper';
/* eslint-enable no-unused-vars */
/**
* Draw layer.
*/
export class DrawLayer {
/**
* The container div.
*
* @type {HTMLDivElement}
*/
#containerDiv;
/**
* Konva stage.
*
* @type {Konva.Stage}
*/
#konvaStage = null;
/**
* The layer base size as {x,y}.
*
* @type {Scalar2D}
*/
#baseSize;
/**
* The layer base spacing as {x,y}.
*
* @type {Scalar2D}
*/
#baseSpacing;
/**
* The layer fit scale.
*
* @type {Scalar2D}
*/
#fitScale = {x: 1, y: 1};
/**
* The layer flip scale.
*
* @type {Scalar3D}
*/
#flipScale = {x: 1, y: 1, z: 1};
/**
* The base layer offset.
*
* @type {Scalar2D}
*/
#baseOffset = {x: 0, y: 0};
/**
* The view offset.
*
* @type {Scalar2D}
*/
#viewOffset = {x: 0, y: 0};
/**
* The zoom offset.
*
* @type {Scalar2D}
*/
#zoomOffset = {x: 0, y: 0};
/**
* The flip offset.
*
* @type {Scalar2D}
*/
#flipOffset = {x: 0, y: 0};
/**
* The draw controller.
*
* @type {object}
*/
#drawController;
/**
* The plane helper.
*
* @type {PlaneHelper}
*/
#planeHelper;
/**
* The associated data id.
*
* @type {string}
*/
#dataId;
/**
* @param {HTMLDivElement} containerDiv The layer div, its id will be used
* as this layer id.
*/
constructor(containerDiv) {
this.#containerDiv = containerDiv;
// specific css class name
this.#containerDiv.className += ' drawLayer';
}
/**
* Get the associated data id.
*
* @returns {string} The id.
*/
getDataId() {
return this.#dataId;
}
/**
* Listener handler.
*
* @type {ListenerHandler}
*/
#listenerHandler = new ListenerHandler();
/**
* Get the Konva stage.
*
* @returns {Konva.Stage} The stage.
*/
getKonvaStage() {
return this.#konvaStage;
}
/**
* Get the Konva layer.
*
* @returns {Konva.Layer} The layer.
*/
getKonvaLayer() {
// there should only be one layer
return this.#konvaStage.getLayers()[0];
}
/**
* Get the draw controller.
*
* @returns {object} The controller.
*/
getDrawController() {
return this.#drawController;
}
/**
* Set the plane helper.
*
* @param {PlaneHelper} helper The helper.
*/
setPlaneHelper(helper) {
this.#planeHelper = helper;
}
// common layer methods [start] ---------------
/**
* Get the id of the layer.
*
* @returns {string} The string id.
*/
getId() {
return this.#containerDiv.id;
}
/**
* Remove the HTML element from the DOM.
*/
removeFromDOM() {
this.#containerDiv.remove();
}
/**
* Get the layer base size (without scale).
*
* @returns {Scalar2D} The size as {x,y}.
*/
getBaseSize() {
return this.#baseSize;
}
/**
* Get the layer opacity.
*
* @returns {number} The opacity ([0:1] range).
*/
getOpacity() {
return this.#konvaStage.opacity();
}
/**
* Set the layer opacity.
*
* @param {number} alpha The opacity ([0:1] range).
*/
setOpacity(alpha) {
this.#konvaStage.opacity(Math.min(Math.max(alpha, 0), 1));
}
/**
* Add a flip offset along the layer X axis.
*/
addFlipOffsetX() {
// flip offset
const scale = this.#konvaStage.scale();
const size = this.#konvaStage.size();
this.#flipOffset.x += size.width / scale.x;
// apply
const offset = this.#konvaStage.offset();
offset.x += this.#flipOffset.x;
this.#konvaStage.offset(offset);
}
/**
* Add a flip offset along the layer Y axis.
*/
addFlipOffsetY() {
// flip offset
const scale = this.#konvaStage.scale();
const size = this.#konvaStage.size();
this.#flipOffset.y += size.height / scale.y;
// apply
const offset = this.#konvaStage.offset();
offset.y += this.#flipOffset.y;
this.#konvaStage.offset(offset);
}
/**
* Flip the scale along the layer X axis.
*/
flipScaleX() {
this.#flipScale.x *= -1;
}
/**
* Flip the scale along the layer Y axis.
*/
flipScaleY() {
this.#flipScale.y *= -1;
}
/**
* Flip the scale along the layer Z axis.
*/
flipScaleZ() {
this.#flipScale.z *= -1;
}
/**
* Set the layer scale.
*
* @param {Scalar3D} newScale The scale as {x,y,z}.
* @param {Point3D} [center] The scale center.
*/
setScale(newScale, center) {
const orientedNewScale =
this.#planeHelper.getTargetOrientedPositiveXYZ({
x: newScale.x * this.#flipScale.x,
y: newScale.y * this.#flipScale.y,
z: newScale.z * this.#flipScale.z,
});
const finalNewScale = {
x: this.#fitScale.x * orientedNewScale.x,
y: this.#fitScale.y * orientedNewScale.y
};
const offset = this.#konvaStage.offset();
if (Math.abs(newScale.x) === 1 &&
Math.abs(newScale.y) === 1 &&
Math.abs(newScale.z) === 1) {
// reset zoom offset for scale=1
const resetOffset = {
x: offset.x - this.#zoomOffset.x,
y: offset.y - this.#zoomOffset.y
};
// store new offset
this.#zoomOffset = {x: 0, y: 0};
this.#konvaStage.offset(resetOffset);
} else {
if (typeof center !== 'undefined') {
let worldCenter = this.#planeHelper.getPlaneOffsetFromOffset3D({
x: center.getX(),
y: center.getY(),
z: center.getZ()
});
// center was obtained with viewLayer.displayToMainPlanePos
// compensated for baseOffset
// TODO: justify...
worldCenter = {
x: worldCenter.x + this.#baseOffset.x,
y: worldCenter.y + this.#baseOffset.y
};
const newOffset = getScaledOffset(
offset, this.#konvaStage.scale(), finalNewScale, worldCenter);
const newZoomOffset = {
x: this.#zoomOffset.x + newOffset.x - offset.x,
y: this.#zoomOffset.y + newOffset.y - offset.y
};
// store new offset
this.#zoomOffset = newZoomOffset;
this.#konvaStage.offset(newOffset);
}
}
this.#konvaStage.scale(finalNewScale);
// update labels
this.#updateLabelScale(finalNewScale);
}
/**
* Set the layer offset.
*
* @param {Scalar3D} newOffset The offset as {x,y,z}.
*/
setOffset(newOffset) {
const planeNewOffset =
this.#planeHelper.getPlaneOffsetFromOffset3D(newOffset);
this.#konvaStage.offset({
x: planeNewOffset.x +
this.#viewOffset.x +
this.#baseOffset.x +
this.#zoomOffset.x +
this.#flipOffset.x,
y: planeNewOffset.y +
this.#viewOffset.y +
this.#baseOffset.y +
this.#zoomOffset.y +
this.#flipOffset.y
});
}
/**
* Set the base layer offset. Updates the layer offset.
*
* @param {Vector3D} scrollOffset The scroll offset vector.
* @param {Vector3D} planeOffset The plane offset vector.
* @returns {boolean} True if the offset was updated.
*/
setBaseOffset(scrollOffset, planeOffset) {
const scrollIndex = this.#planeHelper.getNativeScrollIndex();
const newOffset = this.#planeHelper.getPlaneOffsetFromOffset3D({
x: scrollIndex === 0 ? scrollOffset.getX() : planeOffset.getX(),
y: scrollIndex === 1 ? scrollOffset.getY() : planeOffset.getY(),
z: scrollIndex === 2 ? scrollOffset.getZ() : planeOffset.getZ(),
});
const needsUpdate = this.#baseOffset.x !== newOffset.x ||
this.#baseOffset.y !== newOffset.y;
// reset offset if needed
if (needsUpdate) {
const offset = this.#konvaStage.offset();
this.#konvaStage.offset({
x: offset.x - this.#baseOffset.x + newOffset.x,
y: offset.y - this.#baseOffset.y + newOffset.y
});
this.#baseOffset = newOffset;
}
return needsUpdate;
}
/**
* Display the layer.
*
* @param {boolean} flag Whether to display the layer or not.
*/
display(flag) {
this.#containerDiv.style.display = flag ? '' : 'none';
}
/**
* Check if the layer is visible.
*
* @returns {boolean} True if the layer is visible.
*/
isVisible() {
return this.#containerDiv.style.display === '';
}
/**
* Draw the content (imageData) of the layer.
* The imageData variable needs to be set.
*/
draw() {
this.#konvaStage.draw();
}
/**
* Initialise the layer: set the canvas and context.
*
* @param {Scalar2D} size The image size as {x,y}.
* @param {Scalar2D} spacing The image spacing as {x,y}.
* @param {string} dataId The associated data id.
*/
initialise(size, spacing, dataId) {
// set locals
this.#baseSize = size;
this.#baseSpacing = spacing;
this.#dataId = dataId;
// create stage
this.#konvaStage = new Konva.Stage({
container: this.#containerDiv,
width: this.#baseSize.x,
height: this.#baseSize.y,
listening: false
});
// reset style
// (avoids a not needed vertical scrollbar)
this.#konvaStage.getContent().setAttribute('style', '');
// create layer
const konvaLayer = new Konva.Layer({
listening: false,
visible: true
});
this.#konvaStage.add(konvaLayer);
// create draw controller
this.#drawController = new DrawController(this);
}
/**
* Fit the layer to its parent container.
*
* @param {Scalar2D} containerSize The container size as {x,y}.
* @param {number} divToWorldSizeRatio The div to world size ratio.
* @param {Scalar2D} fitOffset The fit offset as {x,y}.
*/
fitToContainer(containerSize, divToWorldSizeRatio, fitOffset) {
// update konva
this.#konvaStage.width(containerSize.x);
this.#konvaStage.height(containerSize.y);
// fit scale
const divToImageSizeRatio = {
x: divToWorldSizeRatio * this.#baseSpacing.x,
y: divToWorldSizeRatio * this.#baseSpacing.y
};
// #scale = inputScale * fitScale * flipScale
// flipScale does not change here, we can omit it
// newScale = (#scale / fitScale) * newFitScale
const newScale = {
x: this.#konvaStage.scale().x * divToImageSizeRatio.x / this.#fitScale.x,
y: this.#konvaStage.scale().y * divToImageSizeRatio.y / this.#fitScale.y
};
// set scales if different from previous
if (this.#konvaStage.scale().x !== newScale.x ||
this.#konvaStage.scale().y !== newScale.y) {
this.#fitScale = divToImageSizeRatio;
this.#konvaStage.scale(newScale);
}
// view offset
const newViewOffset = {
x: fitOffset.x / divToImageSizeRatio.x,
y: fitOffset.y / divToImageSizeRatio.y
};
// flip offset
const scaledImageSize = {
x: containerSize.x / divToImageSizeRatio.x,
y: containerSize.y / divToImageSizeRatio.y
};
const newFlipOffset = {
x: this.#flipOffset.x !== 0 ? scaledImageSize.x : 0,
y: this.#flipOffset.y !== 0 ? scaledImageSize.y : 0,
};
// set offsets if different from previous
if (this.#viewOffset.x !== newViewOffset.x ||
this.#viewOffset.y !== newViewOffset.y ||
this.#flipOffset.x !== newFlipOffset.x ||
this.#flipOffset.y !== newFlipOffset.y) {
// update global offset
this.#konvaStage.offset({
x: this.#konvaStage.offset().x +
newViewOffset.x - this.#viewOffset.x +
newFlipOffset.x - this.#flipOffset.x,
y: this.#konvaStage.offset().y +
newViewOffset.y - this.#viewOffset.y +
newFlipOffset.y - this.#flipOffset.y,
});
// update private local offsets
this.#flipOffset = newFlipOffset;
this.#viewOffset = newViewOffset;
}
}
/**
* Check the visibility of a given group.
*
* @param {string} id The id of the group.
* @returns {boolean} True if the group is visible.
*/
isGroupVisible(id) {
// get the group
const group = this.#drawController.getGroup(id);
if (typeof group === 'undefined') {
return false;
}
// get visibility
return group.isVisible();
}
/**
* Toggle the visibility of a given group.
*
* @param {string} id The id of the group.
* @returns {boolean} False if the group cannot be found.
*/
toggleGroupVisibility(id) {
// get the group
const group = this.#drawController.getGroup(id);
if (typeof group === 'undefined') {
return false;
}
// toggle visible
group.visible(!group.isVisible());
// udpate
this.draw();
return true;
}
/**
* Delete a Draw from the stage.
*
* @param {string} id The id of the group to delete.
* @param {object} exeCallback The callback to call once the
* DeleteCommand has been executed.
*/
deleteDraw(id, exeCallback) {
if (typeof this.#drawController !== 'undefined') {
this.#drawController.deleteDraw(id, this.#fireEvent, exeCallback);
}
}
/**
* Delete all Draws from the stage.
*
* @param {object} exeCallback The callback to call once the
* DeleteCommand has been executed.
*/
deleteDraws(exeCallback) {
if (typeof this.#drawController !== 'undefined') {
this.#drawController.deleteDraws(this.#fireEvent, exeCallback);
}
}
/**
* Get the total number of draws of this layer
* (at all positions).
*
* @returns {number|undefined} The total number of draws.
*/
getNumberOfDraws() {
let res;
if (typeof this.#drawController !== 'undefined') {
res = this.#drawController.getNumberOfDraws();
}
return res;
}
/**
* Enable and listen to container interaction events.
*/
bindInteraction() {
this.#konvaStage.listening(true);
// allow pointer events
this.#containerDiv.style.pointerEvents = 'auto';
// interaction events
const names = InteractionEventNames;
for (let i = 0; i < names.length; ++i) {
this.#containerDiv.addEventListener(names[i], this.#fireEvent);
}
}
/**
* Disable and stop listening to container interaction events.
*/
unbindInteraction() {
this.#konvaStage.listening(false);
// disable pointer events
this.#containerDiv.style.pointerEvents = 'none';
// interaction events
const names = InteractionEventNames;
for (let i = 0; i < names.length; ++i) {
this.#containerDiv.removeEventListener(names[i], this.#fireEvent);
}
}
/**
* Set the current position.
*
* @param {Point} position The new position.
* @param {Index} index The new index.
* @returns {boolean} True if the position was updated.
*/
setCurrentPosition(position, index) {
this.getDrawController().activateDrawLayer(
index, this.#planeHelper.getScrollIndex());
// TODO: add check
return true;
}
/**
* Add an event listener to this class.
*
* @param {string} type The event type.
* @param {Function} callback The function associated with the provided
* event type, will be called with the fired event.
*/
addEventListener(type, callback) {
this.#listenerHandler.add(type, callback);
}
/**
* Remove an event listener from this class.
*
* @param {string} type The event type.
* @param {Function} callback The function associated with the provided
* event type.
*/
removeEventListener(type, callback) {
this.#listenerHandler.remove(type, callback);
}
/**
* Fire an event: call all associated listeners with the input event object.
*
* @param {object} event The event to fire.
*/
#fireEvent = (event) => {
event.srclayerid = this.getId();
event.dataid = this.#dataId;
this.#listenerHandler.fireEvent(event);
};
// common layer methods [end] ---------------
/**
* Update label scale: compensate for it so
* that label size stays visually the same.
*
* @param {Scalar2D} scale The scale to compensate for as {x,y}.
*/
#updateLabelScale(scale) {
// same formula as in style::applyZoomScale:
// compensate for scale and times 2 so that font 10 looks like a 10
const ratioX = 2 / scale.x;
const ratioY = 2 / scale.y;
// compensate scale for labels
const labels = this.#konvaStage.find('Label');
for (let i = 0; i < labels.length; ++i) {
labels[i].scale({x: ratioX, y: ratioY});
}
}
} // DrawLayer class