src/widgets/widgets.velocityTimeIntegral.js
import { widgetsBase } from './widgets.base';
import { widgetsHandle as widgetsHandleFactory } from './widgets.handle';
import CoreUtils from '../core/core.utils';
/**
* @module widgets/velocityTimeIntegral
*/
const widgetsVelocityTimeIntegral = (three = window.THREE) => {
if (three === undefined || three.Object3D === undefined) {
return null;
}
const Constructor = widgetsBase(three);
return class extends Constructor {
constructor(targetMesh, controls, params = {}) {
super(targetMesh, controls, params);
this._widgetType = 'VelocityTimeIntegral';
// incoming parameters (+ ijk2LPS, lps2IJK, worldPosition)
this._regions = params.ultrasoundRegions || []; // required
if (this._regions.length < 1) {
throw new Error('Ultrasound regions should not be empty!');
}
// outgoing values
this._vMax = null; // Maximum Velocity (Vmax)
this._vMean = null; // Mean Velocity (Vmean)
this._gMax = null; // Maximum Gradient (Gmax)
this._gMean = null; // Mean Gradient (Gmean)
this._envTi = null; // Envelope Duration (Env.Ti)
this._vti = null; // Velocity Time Integral (VTI)
this._extraInfo = null; // extra information which is added to label
this._initialized = false; // set to true onEnd if number of handles > 2
this._isHandleActive = true;
this._domHovered = false;
this._initialRegion = this.getRegionByXY(
this._regions,
CoreUtils.worldToData(params.lps2IJK, params.worldPosition)
);
if (this._initialRegion === null) {
throw new Error('Invalid initial UltraSound region!');
}
this._usPoints = [];
// mesh stuff
this._material = null;
this._geometry = null;
this._mesh = null;
// dom stuff
this._lines = [];
this._label = null;
// add handles
this._handles = [];
const WidgetsHandle = widgetsHandleFactory(three);
let handle = new WidgetsHandle(targetMesh, controls, params);
this.add(handle);
this._handles.push(handle);
this._moveHandle = new WidgetsHandle(targetMesh, controls, params);
this.add(this._moveHandle);
this._moveHandle.hide();
this.onMove = this.onMove.bind(this);
this.onHover = this.onHover.bind(this);
this.create();
this.addEventListeners();
}
addEventListeners() {
this._container.addEventListener('wheel', this.onMove);
this._label.addEventListener('mouseenter', this.onHover);
this._label.addEventListener('mouseleave', this.onHover);
}
removeEventListeners() {
this._container.removeEventListener('wheel', this.onMove);
this._label.removeEventListener('mouseenter', this.onHover);
this._label.removeEventListener('mouseleave', this.onHover);
}
onHover(evt) {
if (evt) {
this.hoverDom(evt);
}
this.hoverMesh();
let hovered = false;
this._handles.forEach(elem => (hovered = hovered || elem.hovered));
this._hovered = hovered || this._domHovered;
this._container.style.cursor = this._hovered ? 'pointer' : 'default';
}
hoverMesh() {
// check raycast intersection, if we want to hover on mesh instead of just css
}
hoverDom(evt) {
this._domHovered = evt.type === 'mouseenter';
}
onStart(evt) {
let active = false;
this._moveHandle.onMove(evt, true);
this._handles.forEach(elem => {
elem.onStart(evt);
active = active || elem.active;
});
this._active = active || this._domHovered;
this._isHandleActive = active;
if (this._domHovered) {
this._controls.enabled = false;
}
this.update();
}
onMove(evt) {
if (this.active) {
const prevPosition = this._moveHandle.worldPosition.clone();
this._moveHandle.onMove(evt, true);
const shift = this._moveHandle.worldPosition.clone().sub(prevPosition);
if (!this.isCorrectRegion(shift)) {
this._moveHandle.worldPosition.copy(prevPosition);
return;
}
if (!this._initialized) {
this._handles[this._handles.length - 1].hovered = false;
this._handles[this._handles.length - 1].active = false;
this._handles[this._handles.length - 1].tracking = false;
const WidgetsHandle = widgetsHandleFactory(three);
let handle = new WidgetsHandle(this._targetMesh, this._controls, this._params);
handle.hovered = true;
handle.active = true;
handle.tracking = true;
this.add(handle);
this._handles.push(handle);
this.createLine();
} else {
this.updateDOMContent(true);
if (
!this._isHandleActive ||
this._handles[this._handles.length - 2].active ||
this._handles[this._handles.length - 1].active
) {
this._handles.forEach(handle => {
handle.worldPosition.add(shift);
});
this._isHandleActive = false;
this._handles[this._handles.length - 2].active = false;
this._handles[this._handles.length - 1].active = false;
this._controls.enabled = false;
}
}
this._dragged = true;
} else {
this.onHover(null);
}
this._handles.forEach(elem => {
elem.onMove(evt);
});
if (this.active && this._handles.length > 2) {
this.pushPopHandle();
}
this.update();
}
onEnd() {
if (this._handles.length < 3) {
return;
}
let active = false;
this._handles.slice(0, -1).forEach(elem => {
elem.onEnd();
active = active || elem.active;
});
// Last Handle
if (this._dragged || !this._handles[this._handles.length - 1].tracking) {
this._handles[this._handles.length - 1].tracking = false;
this._handles[this._handles.length - 1].onEnd();
} else {
this._handles[this._handles.length - 1].tracking = false;
}
if (this._dragged || !this._initialized) {
this.finalize();
this.updateDOMContent();
}
if (!this._dragged && this._active) {
this._selected = !this._selected; // change state if there was no dragging
this._handles.forEach(elem => (elem.selected = this._selected));
}
this._active = active || this._handles[this._handles.length - 1].active;
this._isHandleActive = active;
this._dragged = false;
this._initialized = true;
this.update();
}
isCorrectRegion(shift) {
let isCorrect = true;
this._handles.forEach((handle, index) => {
if (handle.active || !this._isHandleActive) {
isCorrect = isCorrect && this.checkHandle(index, shift);
}
});
return isCorrect;
}
checkHandle(index, shift) {
const region = this.getRegionByXY(
this._regions,
CoreUtils.worldToData(
this._params.lps2IJK,
this._handles[index].worldPosition.clone().add(shift)
)
);
return (
region !== null &&
region === this._initialRegion &&
this._regions[region].unitsY === 'cm/sec'
);
}
create() {
this.createMaterial();
this.createDOM();
}
createMaterial() {
this._material = new three.LineBasicMaterial();
}
createDOM() {
this._label = document.createElement('div');
this._label.className = 'widgets-label';
const measurementsContainer = document.createElement('div');
['vmax', 'vmean', 'gmax', 'gmean', 'envti', 'vti', 'info'].forEach(name => {
const div = document.createElement('div');
div.className = name;
measurementsContainer.appendChild(div);
});
this._label.appendChild(measurementsContainer);
this._container.appendChild(this._label);
this.updateDOMColor();
}
createLine() {
const line = document.createElement('div');
line.className = 'widgets-line';
line.addEventListener('mouseenter', this.onHover);
line.addEventListener('mouseleave', this.onHover);
this._lines.push(line);
this._container.appendChild(line);
}
pushPopHandle() {
let handle0 = this._handles[this._handles.length - 3];
let handle1 = this._handles[this._handles.length - 2];
let newhandle = this._handles[this._handles.length - 1];
let isOnLine = this.isPointOnLine(
handle0.worldPosition,
handle1.worldPosition,
newhandle.worldPosition
);
if (isOnLine || handle0.screenPosition.distanceTo(newhandle.screenPosition) < 25) {
this.remove(handle1);
handle1.free();
this._handles[this._handles.length - 2] = newhandle;
this._handles.pop();
this._container.removeChild(this._lines.pop());
}
return isOnLine;
}
isPointOnLine(pointA, pointB, pointToCheck) {
return !new three.Vector3()
.crossVectors(pointA.clone().sub(pointToCheck), pointB.clone().sub(pointToCheck))
.length();
}
finalize() {
if (this._initialized) {
// remove old axis handles
this._handles.splice(-2).forEach(elem => {
this.remove(elem);
elem.free();
});
}
const pointF = CoreUtils.worldToData(this._params.lps2IJK, this._handles[0]._worldPosition);
const pointL = CoreUtils.worldToData(
this._params.lps2IJK,
this._handles[this._handles.length - 1]._worldPosition
);
const region = this._regions[this.getRegionByXY(this._regions, pointF)];
const axisY = region.y0 + (region.axisY || 0); // data coordinate equal to US region's zero Y coordinate
const WidgetsHandle = widgetsHandleFactory(three);
const params = { hideHandleMesh: this._params.hideHandleMesh || false };
pointF.y = axisY;
pointL.y = axisY;
this._usPoints = [
this.getPointInRegion(region, pointL),
this.getPointInRegion(region, pointF),
];
params.worldPosition = pointL.applyMatrix4(this._params.ijk2LPS); // projection of last point on Y axis
this._handles.push(new WidgetsHandle(this._targetMesh, this._controls, params));
this.add(this._handles[this._handles.length - 1]);
params.worldPosition = pointF.applyMatrix4(this._params.ijk2LPS); // projection of first point on Y axis
this._handles.push(new WidgetsHandle(this._targetMesh, this._controls, params));
this.add(this._handles[this._handles.length - 1]);
while (this._lines.length < this._handles.length) {
this.createLine();
}
}
update() {
this.updateColor();
// update handles
this._handles.forEach(elem => elem.update());
// mesh stuff
this.updateMesh();
// DOM stuff
this.updateDOMColor();
this.updateDOMPosition();
}
updateValues() {
const region = this._regions[
this.getRegionByXY(
this._regions,
CoreUtils.worldToData(this._params.lps2IJK, this._handles[0]._worldPosition)
)
];
const boundaries = {
xMin: Number.POSITIVE_INFINITY,
xMax: Number.NEGATIVE_INFINITY,
yMin: Number.POSITIVE_INFINITY,
yMax: Number.NEGATIVE_INFINITY,
};
let pVelocity;
let pGradient;
let pTime;
let totalTime = 0;
this._vMax = 0;
this._vMean = 0;
this._gMean = 0;
this._usPoints.splice(2);
this._handles.slice(0, -2).forEach(elem => {
const usPosition = this.getPointInRegion(
region,
CoreUtils.worldToData(this._params.lps2IJK, elem._worldPosition)
);
const velocity = Math.abs(usPosition.y / 100);
const gradient = 4 * Math.pow(velocity, 2);
if (this._vMax === null || velocity > this._vMax) {
this._vMax = velocity;
}
boundaries.xMin = Math.min(usPosition.x, boundaries.xMin);
boundaries.xMax = Math.max(usPosition.x, boundaries.xMax);
boundaries.yMin = Math.min(usPosition.y, boundaries.yMin);
boundaries.yMax = Math.max(usPosition.y, boundaries.yMax);
if (pTime) {
const length = Math.abs(usPosition.x - pTime);
totalTime += length;
this._vMean += (length * (pVelocity + velocity)) / 2;
this._gMean += (length * (pGradient + gradient)) / 2;
}
pVelocity = velocity;
pGradient = gradient;
pTime = usPosition.x;
this._usPoints.push(usPosition);
});
this._gMax = 4 * Math.pow(this._vMax, 2);
this._vMean /= totalTime;
this._gMean /= totalTime;
this._envTi = totalTime * 1000;
this._vti = this.getArea(this._usPoints);
this._shapeWarn =
boundaries.xMax - boundaries.xMin !== totalTime ||
boundaries.yMin < 0 !== boundaries.yMax < 0;
}
updateMesh() {
if (this._mesh) {
this.remove(this._mesh);
}
this._geometry = new three.Geometry();
this._handles.forEach(elem => this._geometry.vertices.push(elem.worldPosition));
this._geometry.vertices.push(this._handles[0].worldPosition);
this._geometry.verticesNeedUpdate = true;
this.updateMeshColor();
this._mesh = new three.Line(this._geometry, this._material);
this._mesh.visible = true;
this.add(this._mesh);
}
updateMeshColor() {
if (this._material) {
this._material.color.set(this._color);
}
}
updateDOMColor() {
if (this._handles.length >= 2) {
this._lines.forEach(elem => (elem.style.backgroundColor = this._color));
}
this._label.style.borderColor = this._color;
}
updateDOMContent(clear) {
const vMaxContainer = this._label.querySelector('.vmax');
const vMeanContainer = this._label.querySelector('.vmean');
const gMaxContainer = this._label.querySelector('.gmax');
const gMeanContainer = this._label.querySelector('.gmean');
const envTiContainer = this._label.querySelector('.envti');
const vtiContainer = this._label.querySelector('.vti');
const infoContainer = this._label.querySelector('.info');
if (clear) {
vMaxContainer.innerHTML = '';
vMeanContainer.innerHTML = '';
gMaxContainer.innerHTML = '';
gMeanContainer.innerHTML = '';
envTiContainer.innerHTML = '';
vtiContainer.innerHTML = '';
infoContainer.innerHTML = '';
return;
}
this.updateValues();
if (this._shapeWarn && !this._label.hasAttribute('title')) {
this._label.setAttribute('title', 'Values may be incorrect due to invalid curve.');
this._label.style.color = this._colors.error;
} else if (!this._shapeWarn && this._label.hasAttribute('title')) {
this._label.removeAttribute('title');
this._label.style.color = this._colors.text;
}
vMaxContainer.innerHTML = `Vmax: ${this._vMax.toFixed(2)} m/s`;
vMeanContainer.innerHTML = `Vmean: ${this._vMean.toFixed(2)} m/s`;
gMaxContainer.innerHTML = `Gmax: ${this._gMax.toFixed(2)} mmhg`;
gMeanContainer.innerHTML = `Gmean: ${this._gMean.toFixed(2)} mmhg`;
envTiContainer.innerHTML = `Env.Ti: ${this._envTi.toFixed(1)} ms`;
vtiContainer.innerHTML = `VTI: ${this._vti.toFixed(2)} cm`;
infoContainer.innerHTML = this._extraInfo;
}
updateDOMPosition() {
if (this._handles.length < 2) {
return;
}
// update lines and get coordinates of lowest handle
let labelPosition = null;
this._lines.forEach((elem, ind) => {
const lineData = this.getLineData(
this._handles[ind].screenPosition,
this._handles[ind + 1 === this._handles.length ? 0 : ind + 1].screenPosition
);
elem.style.transform = `translate3D(${lineData.transformX}px, ${lineData.transformY}px, 0)
rotate(${lineData.transformAngle}rad)`;
elem.style.width = lineData.length + 'px';
if (labelPosition === null || labelPosition.y < this._handles[ind].screenPosition.y) {
labelPosition = this._handles[ind].screenPosition.clone();
}
});
if (!this._initialized) {
return;
}
// update label
labelPosition.y += 15 + this._label.offsetHeight / 2;
labelPosition = this.adjustLabelTransform(this._label, labelPosition);
this._label.style.transform = `translate3D(${labelPosition.x}px, ${labelPosition.y}px, 0)`;
}
hideDOM() {
this._handles.forEach(elem => elem.hideDOM());
this._lines.forEach(elem => (elem.style.display = 'none'));
this._label.style.display = 'none';
}
showDOM() {
this._handles.forEach(elem => elem.showDOM());
this._lines.forEach(elem => (elem.style.display = ''));
this._label.style.display = '';
}
free() {
this.removeEventListeners();
this._handles.forEach(elem => {
this.remove(elem);
elem.free();
});
this._handles = [];
this._usPoints = [];
this.remove(this._moveHandle);
this._moveHandle.free();
this._moveHandle = null;
this._lines.forEach(elem => {
elem.removeEventListener('mouseenter', this.onHover);
elem.removeEventListener('mouseleave', this.onHover);
this._container.removeChild(elem);
});
this._lines = [];
this._container.removeChild(this._label);
// mesh, geometry, material
if (this._mesh) {
this.remove(this._mesh);
this._mesh.geometry.dispose();
this._mesh.geometry = null;
this._mesh.material.dispose();
this._mesh.material = null;
this._mesh = null;
}
if (this._geometry) {
this._geometry.dispose();
this._geometry = null;
}
this._material.vertexShader = null;
this._material.fragmentShader = null;
this._material.uniforms = null;
this._material.dispose();
this._material = null;
super.free();
}
getMeasurements() {
return {
vMax: this._vMax,
vMean: this._vMean,
gMax: this._gMax,
gMean: this._gMean,
envTi: this._envTi,
vti: this._vti,
};
}
get targetMesh() {
return this._targetMesh;
}
set targetMesh(targetMesh) {
this._targetMesh = targetMesh;
this._handles.forEach(elem => (elem.targetMesh = targetMesh));
this._moveHandle.targetMesh = targetMesh;
this.update();
}
get worldPosition() {
return this._worldPosition;
}
set worldPosition(worldPosition) {
this._handles.forEach(elem => elem._worldPosition.copy(worldPosition));
this._worldPosition.copy(worldPosition);
this.update();
}
get extraInfo() {
return this._extraInfo;
}
set extraInfo(info) {
this._extraInfo = info;
this._label.querySelector('.info').innerHTML = info;
}
};
};
export { widgetsVelocityTimeIntegral };
export default widgetsVelocityTimeIntegral();