UNPKG

@cornerstonejs/core

Version:
431 lines (430 loc) • 16.3 kB
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;