@cornerstonejs/core
Version:
Cornerstone3D Core
431 lines (430 loc) • 16.3 kB
JavaScript
import { Events as EVENTS } from '../enums/index.js';
import { Transform } from './helpers/cpuFallback/rendering/transform.js';
import triggerEvent from '../utilities/triggerEvent.js';
import Viewport from './Viewport.js';
import { getOrCreateCanvas } from './helpers/index.js';
import { getGenericViewportSourceDataId } from './GenericViewport/genericViewportDisplaySetAccess.js';
import { ECG_CHANNEL_SPACING, computeECGChannelLayouts, computeECGHeight, drawECGGrid, drawECGLabels, drawECGTraces, ECG_RENDERING_COLORS, ECG_SECONDS_WIDTH, getVisibleECGChannelsByFlag, loadECGWaveform, } from '../utilities/ECGUtilities.js';
const ECG_AMPLITUDE_INDEX_SIZE = 65536;
class ECGViewport extends Viewport {
constructor(props) {
super({
...props,
canvas: props.canvas || getOrCreateCanvas(props.element),
});
this.imageId = null;
this.channels = [];
this.waveformData = null;
this.ecgWidth = 0;
this.ecgHeight = 0;
this.channelScale = 0;
this.ecgCamera = {
panWorld: [0, 0],
parallelScale: 1,
};
this.getProperties = () => {
return {
visibleChannels: this.channels
.map((ch, i) => (ch.visible ? i : -1))
.filter((i) => i >= 0),
};
};
this.resetCamera = () => {
this.refreshRenderValues();
this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.renderFrame();
return true;
};
this.getFrameOfReferenceUID = () => {
return `ecg-viewport-${this.id}`;
};
this.resize = () => {
const canvas = this.canvas;
const { clientWidth, clientHeight } = canvas;
if (canvas.width !== clientWidth || canvas.height !== clientHeight) {
canvas.width = clientWidth;
canvas.height = clientHeight;
}
if (this.waveformData) {
this.computeChannelScale();
this.recalculateHeight();
}
this.refreshRenderValues();
this.renderFrame();
};
this.canvasToWorld = (canvasPos, destPos = [0, 0, 0]) => {
if (!this.waveformData) {
destPos[0] = 0;
destPos[1] = 0;
destPos[2] = 0;
return destPos;
}
const scale = this.getWorldToCanvasRatio();
const pan = this.ecgCamera.panWorld;
const layouts = this.computeChannelLayouts();
const subCanvasPos = [
canvasPos[0] / scale - pan[0],
canvasPos[1] / scale - pan[1],
];
let z = 0;
for (let i = 0; i < layouts.length; i++) {
const layout = layouts[i];
if (subCanvasPos[1] <= layout.yOffset) {
z = i;
break;
}
if (i === layouts.length - 1) {
z = i;
}
}
const x = Math.max(0, Math.min(this.waveformData.numberOfSamples - 1, (subCanvasPos[0] * this.waveformData.numberOfSamples) / this.ecgWidth));
const layout = layouts[z];
const y = (layout.baseline - subCanvasPos[1]) / this.channelScale;
destPos[0] = x;
destPos[1] = y;
destPos[2] = z;
return destPos;
};
this.worldToCanvas = (worldPos) => {
if (!this.waveformData) {
return [0, 0];
}
const scale = this.getWorldToCanvasRatio();
const pan = this.ecgCamera.panWorld;
const layouts = this.computeChannelLayouts();
const z = Math.round(worldPos[2]);
if (z < 0 || z >= layouts.length) {
return [0, 0];
}
const layout = layouts[z];
const canvasX = (worldPos[0] / this.waveformData.numberOfSamples) *
this.ecgWidth *
scale +
pan[0] * scale;
const canvasY = (layout.baseline - worldPos[1] * this.channelScale) * scale +
pan[1] * scale;
return [canvasX, canvasY];
};
this.getRotation = () => 0;
this.getNumberOfSlices = () => {
return 1;
};
this.getCurrentImageIdIndex = () => {
return 0;
};
this.getCurrentImageId = () => {
return this.imageId;
};
this.getSliceIndex = () => {
return 0;
};
this.getImageIds = () => {
return this.imageId ? [this.imageId] : [];
};
this.scroll = () => {
};
this.customRenderViewportToCanvas = () => {
this.renderFrame();
};
this.renderFrame = () => {
if (!this.waveformData) {
return;
}
const dpr = window.devicePixelRatio || 1;
const transform = this.getTransform();
const m = transform.getMatrix();
const ctx = this.canvasContext;
ctx.resetTransform();
ctx.fillStyle = ECG_RENDERING_COLORS.background;
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
ctx.setTransform(m[0] / dpr, m[1] / dpr, m[2] / dpr, m[3] / dpr, m[4] / dpr, m[5] / dpr);
const layouts = this.computeChannelLayouts();
this.drawGrid(ctx);
this.drawTraces(ctx, layouts);
this.drawLabels(ctx, layouts);
ctx.resetTransform();
this.setRendered();
triggerEvent(this.element, EVENTS.IMAGE_RENDERED, {
element: this.element,
viewportId: this.id,
viewport: this,
renderingEngineId: this.renderingEngineId,
});
};
this.getMiddleSliceData = () => {
throw new Error('Method not implemented for ECG viewport.');
};
this.canvasContext = this.canvas.getContext('2d');
this.renderingEngineId = props.renderingEngineId;
this.element.setAttribute('data-viewport-uid', this.id);
this.element.setAttribute('data-rendering-engine-uid', this.renderingEngineId);
this.addEventListeners();
this.resize();
}
static get useCustomRenderingPipeline() {
return true;
}
addEventListeners() {
this.canvas.addEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
}
removeEventListeners() {
this.canvas.removeEventListener(EVENTS.ELEMENT_DISABLED, this.elementDisabledHandler);
}
elementDisabledHandler() {
this.removeEventListeners();
}
async setEcg(imageId) {
this.clearDisplaySets();
this.imageId = imageId;
const { waveform, calibration } = await loadECGWaveform(imageId);
this.channels = waveform.channels.map((channel) => ({
...channel,
visible: true,
}));
this.waveformData = {
...waveform,
channels: this.channels,
multiplexGroupLabel: waveform.multiplexGroupLabel,
};
this.calibration = calibration;
this.ecgWidth = Math.ceil((this.waveformData.numberOfSamples * ECG_SECONDS_WIDTH) /
this.waveformData.samplingFrequency);
this.computeChannelScale();
this.recalculateHeight();
this.refreshRenderValues();
this.renderFrame();
}
async setDisplaySets(...entries) {
await this.mountDisplaySets(entries, async (entry) => {
const sourceDataId = getGenericViewportSourceDataId(entry.displaySetId);
await this.setEcg(sourceDataId);
});
}
setChannelVisibility(index, visible) {
if (index >= 0 && index < this.channels.length) {
this.channels[index].visible = visible;
this.computeChannelScale();
this.recalculateHeight();
this.refreshRenderValues();
this.renderFrame();
}
}
getVisibleChannels() {
return this.channels.map((ch) => ({
name: ch.name,
visible: ch.visible,
}));
}
getWaveformData() {
return this.waveformData;
}
getContentDimensions() {
return { width: this.ecgWidth, height: this.ecgHeight };
}
computeChannelScale() {
const visibleChannels = getVisibleECGChannelsByFlag(this.channels);
if (visibleChannels.length === 0 || this.ecgWidth === 0) {
this.channelScale = 0;
return;
}
let maxRange = 1;
for (const channel of visibleChannels) {
const range = channel.max - channel.min;
maxRange = Math.max(maxRange, range);
}
const canvasAspect = this.canvas.offsetHeight && this.canvas.offsetWidth
? this.canvas.offsetHeight / this.canvas.offsetWidth
: 2 / 3;
const targetTotalHeight = this.ecgWidth * canvasAspect;
const totalSpacing = ECG_CHANNEL_SPACING * visibleChannels.length;
const heightPerChannel = (targetTotalHeight - totalSpacing) / visibleChannels.length;
this.channelScale = heightPerChannel / (maxRange * 1.25);
}
recalculateHeight() {
this.ecgHeight = computeECGHeight(getVisibleECGChannelsByFlag(this.channels), this.channelScale);
}
computeChannelLayouts() {
return computeECGChannelLayouts({
visibleChannels: getVisibleECGChannelsByFlag(this.channels),
channelScale: this.channelScale,
});
}
setProperties(props) {
if (props.visibleChannels !== undefined) {
for (let i = 0; i < this.channels.length; i++) {
this.channels[i].visible = props.visibleChannels.includes(i);
}
this.computeChannelScale();
this.recalculateHeight();
this.refreshRenderValues();
this.renderFrame();
}
}
resetProperties() {
for (const channel of this.channels) {
channel.visible = true;
}
this.computeChannelScale();
this.recalculateHeight();
this.refreshRenderValues();
this.renderFrame();
}
setCamera(camera) {
const { parallelScale, focalPoint } = camera;
if (parallelScale) {
this.ecgCamera.parallelScale =
this.element.clientHeight / 2 / parallelScale;
}
if (focalPoint !== undefined) {
const focalPointCanvas = this.worldToCanvas(focalPoint);
const canvasCenter = [
this.element.clientWidth / 2,
this.element.clientHeight / 2,
];
const panWorldDelta = [
(focalPointCanvas[0] - canvasCenter[0]) / this.ecgCamera.parallelScale,
(focalPointCanvas[1] - canvasCenter[1]) / this.ecgCamera.parallelScale,
];
this.ecgCamera.panWorld = [
this.ecgCamera.panWorld[0] - panWorldDelta[0],
this.ecgCamera.panWorld[1] - panWorldDelta[1],
];
}
this.canvasContext.fillStyle = ECG_RENDERING_COLORS.background;
this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.renderFrame();
}
getCamera() {
const { parallelScale } = this.ecgCamera;
const canvasCenter = [
this.element.clientWidth / 2,
this.element.clientHeight / 2,
];
const canvasCenterWorld = this.canvasToWorld(canvasCenter);
return {
parallelProjection: true,
focalPoint: canvasCenterWorld,
position: [0, 0, 0],
viewUp: [0, -1, 0],
parallelScale: this.element.clientHeight / 2 / parallelScale,
viewPlaneNormal: [0, 0, 1],
};
}
getPan() {
const panWorld = this.ecgCamera.panWorld;
return [panWorld[0], panWorld[1]];
}
getViewReferenceId(_specifier) {
return `imageId:${this.imageId}`;
}
hasImageURI(imageURI) {
return this.imageId?.includes(imageURI) ?? false;
}
isReferenceViewable(viewRef) {
if (viewRef.FrameOfReferenceUID &&
viewRef.FrameOfReferenceUID !== this.getFrameOfReferenceUID()) {
return false;
}
return true;
}
updateCameraClippingPlanesAndRange() {
}
refreshRenderValues() {
if (!this.ecgWidth || !this.ecgHeight) {
return;
}
let worldToCanvasRatio = this.canvas.offsetWidth / this.ecgWidth;
if (this.ecgHeight * worldToCanvasRatio > this.canvas.offsetHeight) {
worldToCanvasRatio = this.canvas.offsetHeight / this.ecgHeight;
}
const drawWidth = Math.floor(this.ecgWidth * worldToCanvasRatio);
const drawHeight = Math.floor(this.ecgHeight * worldToCanvasRatio);
const xOffsetCanvas = (this.canvas.offsetWidth - drawWidth) / 2;
const yOffsetCanvas = (this.canvas.offsetHeight - drawHeight) / 2;
const xOffsetWorld = xOffsetCanvas / worldToCanvasRatio;
const yOffsetWorld = yOffsetCanvas / worldToCanvasRatio;
this.ecgCamera.panWorld = [xOffsetWorld, yOffsetWorld];
this.ecgCamera.parallelScale = worldToCanvasRatio;
}
getWorldToCanvasRatio() {
return this.ecgCamera.parallelScale;
}
getTransform() {
const panWorld = this.ecgCamera.panWorld;
const dpr = window.devicePixelRatio || 1;
const worldToCanvasRatio = this.getWorldToCanvasRatio();
const canvasToWorldRatio = 1.0 / worldToCanvasRatio;
const halfCanvas = [
this.canvas.offsetWidth / 2,
this.canvas.offsetHeight / 2,
];
const halfCanvasWorldCoordinates = [
halfCanvas[0] * canvasToWorldRatio,
halfCanvas[1] * canvasToWorldRatio,
];
const transform = new Transform();
transform.scale(dpr, dpr);
transform.translate(halfCanvas[0], halfCanvas[1]);
transform.scale(worldToCanvasRatio, worldToCanvasRatio);
transform.translate(panWorld[0], panWorld[1]);
transform.translate(-halfCanvasWorldCoordinates[0], -halfCanvasWorldCoordinates[1]);
return transform;
}
drawGrid(ctx) {
drawECGGrid(ctx, {
ecgWidth: this.ecgWidth,
ecgHeight: this.ecgHeight,
channelScale: this.channelScale,
});
}
drawTraces(ctx, layouts) {
drawECGTraces({
ctx,
layouts,
ecgWidth: this.ecgWidth,
channelScale: this.channelScale,
});
}
drawLabels(ctx, layouts) {
drawECGLabels(ctx, layouts, this.getWorldToCanvasRatio());
}
getImageData() {
if (!this.waveformData) {
return null;
}
const nSamples = this.waveformData.numberOfSamples;
const nChannels = this.waveformData.numberOfChannels;
const dimensions = [nSamples, ECG_AMPLITUDE_INDEX_SIZE, nChannels];
const spacing = [1, 1, 1];
const origin = [0, 0, 0];
const direction = [1, 0, 0, 0, 1, 0, 0, 0, 1];
const amplitudeOffset = ECG_AMPLITUDE_INDEX_SIZE / 2;
const imageData = {
getDirection: () => direction,
getDimensions: () => dimensions,
getRange: () => [0, 1],
getSpacing: () => spacing,
worldToIndex: (point) => {
return [point[0], point[1] + amplitudeOffset, point[2]];
},
indexToWorld: (point) => {
return [point[0], point[1] - amplitudeOffset, point[2]];
},
};
return {
dimensions,
spacing,
origin,
direction,
imageData,
hasPixelSpacing: false,
calibration: this.calibration,
preScale: { scaled: false },
metadata: { Modality: 'ECG' },
};
}
getSliceViewInfo() {
throw new Error('Method not implemented for ECG viewport.');
}
}
export default ECGViewport;