refactor: viewports store (#1239)

* Organize viewports reducers

* Organize viewports actions

* Add local state to store dom node and remove hack

* Comment usage of dom in vtk

* Fix set of enabledElements

* Fix warning in html viewport

* Update docs for state

* Add commandsmanager to commandsmodule

* refactor: Shift ConnectedViewportDownloadForm to extension-cornerstone (#1224)

* refactor: 💡 Shift ConnectedViewportDownloadForm to its own proj

Shift ConnectedViewportDownloadForm to its own project and use local
state to get enabledElement

Closes: #1206

* Refactor connected component to cornerstone component

* Remove builtin behaviour from toolbarrow
This commit is contained in:
Danny Brown 2019-12-02 14:02:35 -05:00 committed by GitHub
parent 817d43ee62
commit d98889f1eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 394 additions and 348 deletions

View File

@ -0,0 +1,148 @@
import React from 'react';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { utils } from '@ohif/core';
import { getEnabledElement } from './state';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
const activeEnabledElement = getEnabledElement(activeViewportIndex);
const enableViewport = viewportElement => {
if (viewportElement) {
cornerstone.enable(viewportElement);
}
};
const disableViewport = viewportElement => {
if (viewportElement) {
cornerstone.disable(viewportElement);
}
};
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
new Promise(resolve => {
cornerstone.fitToWindow(viewportElement);
viewportElement.addEventListener(
'cornerstoneimagerendered',
function updateViewport(event) {
const enabledElement = cornerstone.getEnabledElement(event.target)
.element;
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = enabledElement.offsetHeight;
let newHeight = enabledElement.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
viewportElement.removeEventListener(
'cornerstoneimagerendered',
updateViewport
);
}
);
});
const loadImage = (activeViewport, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewport && viewportElement) {
const enabledElement = cornerstone.getEnabledElement(activeViewport);
const viewport = Object.assign({}, enabledElement.viewport);
delete viewport.scale;
viewport.translation = {
x: 0,
y: 0,
};
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
cornerstone.displayImage(viewportElement, image);
cornerstone.setViewport(viewportElement, viewport);
cornerstone.resize(viewportElement, true);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ image, width: newWidth, height: newHeight });
});
}
});
const toggleAnnotations = (toggle, viewportElement) => {
cornerstoneTools.store.state.tools.forEach(({ name }) => {
if (toggle) {
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
} else {
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
}
});
};
const downloadBlob = (
filename,
fileType,
viewportElement,
downloadCanvas
) => {
const file = `${filename}.${fileType}`;
const mimetype = `image/${fileType}`;
/* Handles JPEG images for IE11 */
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
const image = downloadCanvas.toDataURL(mimetype, 1);
const blob = utils.b64toBlob(
image.replace('data:image/jpeg;base64,', ''),
mimetype
);
return window.navigator.msSaveBlob(blob, file);
}
viewportElement.querySelector('canvas').toBlob(blob => {
const URLObj = window.URL || window.webkitURL;
const a = document.createElement('a');
a.href = URLObj.createObjectURL(blob);
a.download = file;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
};
return (
<ViewportDownloadForm
onClose={onClose}
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewport={activeEnabledElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
updateViewportPreview={updateViewportPreview}
loadImage={loadImage}
toggleAnnotations={toggleAnnotations}
downloadBlob={downloadBlob}
/>
);
};
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportIndex: PropTypes.number.isRequired,
};
export default CornerstoneViewportDownloadForm;

View File

@ -1,233 +1,260 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
const actions = {
rotateViewport: ({ viewports, rotation }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
const commandsModule = ({ servicesManager }) => {
const actions = {
rotateViewport: ({ viewports, rotation }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.rotation += rotation;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportHorizontal: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.hflip = !viewport.hflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportVertical: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.vflip = !viewport.vflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
scaleViewport: ({ direction, viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
const step = direction * 0.15;
if (enabledElement) {
if (step) {
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.scale += step;
viewport.rotation += rotation;
cornerstone.setViewport(enabledElement, viewport);
} else {
cornerstone.fitToWindow(enabledElement);
}
}
},
resetViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
},
flipViewportHorizontal: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
cornerstone.reset(enabledElement);
}
},
invertViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.hflip = !viewport.hflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
flipViewportVertical: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.invert = !viewport.invert;
cornerstone.setViewport(enabledElement, viewport);
}
},
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
// better mouseButtonMask sets.
setToolActive: ({ toolName }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = getEnabledElement(viewports.activeViewportIndex);
if (!element) {
return;
}
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.vflip = !viewport.vflip;
cornerstone.setViewport(enabledElement, viewport);
}
},
scaleViewport: ({ direction, viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
const step = direction * 0.15;
const enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement || !enabledElement.image) {
return;
}
const {
toolState,
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
if (
!toolState ||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
) {
return;
}
const imageIdToolState = toolState[enabledElement.image.imageId];
const measurementsToRemove = [];
Object.keys(imageIdToolState).forEach(toolType => {
const { data } = imageIdToolState[toolType];
data.forEach(measurementData => {
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
if (!_id) {
return;
if (enabledElement) {
if (step) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.scale += step;
cornerstone.setViewport(enabledElement, viewport);
} else {
cornerstone.fitToWindow(enabledElement);
}
}
},
resetViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
measurementsToRemove.push({
toolType,
_id,
lesionNamingNumber,
measurementNumber,
if (enabledElement) {
cornerstone.reset(enabledElement);
}
},
invertViewport: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.invert = !viewport.invert;
cornerstone.setViewport(enabledElement, viewport);
}
},
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
// better mouseButtonMask sets.
setToolActive: ({ toolName }) => {
if (!toolName) {
console.warn('No toolname provided to setToolActive command');
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = getEnabledElement(viewports.activeViewportIndex);
if (!element) {
return;
}
const enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement || !enabledElement.image) {
return;
}
const {
toolState,
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
if (
!toolState ||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
) {
return;
}
const imageIdToolState = toolState[enabledElement.image.imageId];
const measurementsToRemove = [];
Object.keys(imageIdToolState).forEach(toolType => {
const { data } = imageIdToolState[toolType];
data.forEach(measurementData => {
const {
_id,
lesionNamingNumber,
measurementNumber,
} = measurementData;
if (!_id) {
return;
}
measurementsToRemove.push({
toolType,
_id,
lesionNamingNumber,
measurementNumber,
});
});
});
});
measurementsToRemove.forEach(measurementData => {
OHIF.measurements.MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData,
measurementsToRemove.forEach(measurementData => {
OHIF.measurements.MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData,
},
});
});
},
nextImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, 1);
},
previousImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, -1);
},
getActiveViewportEnabledElement: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
return enabledElement;
},
showDownloadViewportModal: ({ title, viewports }) => {
const activeViewportIndex = viewports.activeViewportIndex;
const { UIModalService } = servicesManager.services;
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
});
});
},
nextImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, 1);
},
previousImage: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
scroll(enabledElement, -1);
},
getActiveViewportEnabledElement: ({ viewports }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
return enabledElement;
},
},
};
const definitions = {
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: ['viewports'],
options: {},
},
getActiveViewportEnabledElement: {
commandFn: actions.getActiveViewportEnabledElement,
storeContexts: ['viewports'],
options: {},
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 },
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: ['viewports'],
options: {},
},
flipViewportVertical: {
commandFn: actions.flipViewportVertical,
storeContexts: ['viewports'],
options: {},
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: ['viewports'],
options: {},
},
scaleUpViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 1 },
},
scaleDownViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: -1 },
},
fitViewportToWindow: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 0 },
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: ['viewports'],
options: {},
},
clearAnnotations: {
commandFn: actions.clearAnnotations,
storeContexts: ['viewports'],
options: {},
},
nextImage: {
commandFn: actions.nextImage,
storeContexts: ['viewports'],
options: {},
},
previousImage: {
commandFn: actions.previousImage,
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
};
};
const definitions = {
getActiveViewportEnabledElement: {
commandFn: actions.getActiveViewportEnabledElement,
storeContexts: ['viewports'],
options: {},
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 },
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: ['viewports'],
options: {},
},
flipViewportVertical: {
commandFn: actions.flipViewportVertical,
storeContexts: ['viewports'],
options: {},
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: ['viewports'],
options: {},
},
scaleUpViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 1 },
},
scaleDownViewport: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: -1 },
},
fitViewportToWindow: {
commandFn: actions.scaleViewport,
storeContexts: ['viewports'],
options: { direction: 0 },
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: ['viewports'],
options: {},
},
clearAnnotations: {
commandFn: actions.clearAnnotations,
storeContexts: ['viewports'],
options: {},
},
nextImage: {
commandFn: actions.nextImage,
storeContexts: ['viewports'],
options: {},
},
previousImage: {
commandFn: actions.previousImage,
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
};
export default {
actions,
definitions,
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
};
export default commandsModule;

View File

@ -2,6 +2,7 @@ import init from './init.js';
import asyncComponent from './asyncComponent.js';
import commandsModule from './commandsModule.js';
import toolbarModule from './toolbarModule.js';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const OHIFCornerstoneViewport = asyncComponent(() =>
import(
@ -33,7 +34,9 @@ export default {
getToolbarModule() {
return toolbarModule;
},
getCommandsModule() {
return commandsModule;
getCommandsModule({ servicesManager }) {
return commandsModule({ servicesManager });
},
};
export { CornerstoneViewportDownloadForm };

View File

@ -1,131 +0,0 @@
import { connect } from 'react-redux';
import { ViewportDownloadForm } from '@ohif/ui';
import { utils } from '@ohif/core';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import { commandsManager } from './../App.js';
const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const mapStateToProps = (state, ownProps) => {
const activeEnabledElement = commandsManager.runCommand(
'getActiveViewportEnabledElement'
);
return {
onClose: ownProps.hide,
minimumSize: MINIMUM_SIZE,
maximumSize: MAX_TEXTURE_SIZE,
defaultSize: DEFAULT_SIZE,
canvasClass: 'cornerstone-canvas',
activeViewport: activeEnabledElement,
enableViewport: viewportElement => {
if (viewportElement) {
cornerstone.enable(viewportElement);
}
},
disableViewport: viewportElement => {
if (viewportElement) {
cornerstone.disable(viewportElement);
}
},
updateViewportPreview: (viewportElement, downloadCanvas, fileType) =>
new Promise(resolve => {
cornerstone.fitToWindow(viewportElement);
viewportElement.addEventListener(
'cornerstoneimagerendered',
function updateViewport(event) {
const enabledElement = cornerstone.getEnabledElement(event.target)
.element;
const type = 'image/' + fileType;
const dataUrl = downloadCanvas.toDataURL(type, 1);
let newWidth = enabledElement.offsetHeight;
let newHeight = enabledElement.offsetWidth;
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
newHeight *= multiplier;
newWidth *= multiplier;
}
resolve({ dataUrl, width: newWidth, height: newHeight });
viewportElement.removeEventListener(
'cornerstoneimagerendered',
updateViewport
);
}
);
}),
loadImage: (activeViewport, viewportElement, width, height) =>
new Promise(resolve => {
if (activeViewport && viewportElement) {
const enabledElement = cornerstone.getEnabledElement(activeViewport);
const viewport = Object.assign({}, enabledElement.viewport);
delete viewport.scale;
viewport.translation = {
x: 0,
y: 0,
};
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
cornerstone.displayImage(viewportElement, image);
cornerstone.setViewport(viewportElement, viewport);
cornerstone.resize(viewportElement, true);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(
height || image.height,
MAX_TEXTURE_SIZE
);
resolve({ image, width: newWidth, height: newHeight });
});
}
}),
toggleAnnotations: (toggle, viewportElement) => {
cornerstoneTools.store.state.tools.forEach(({ name }) => {
if (toggle) {
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
} else {
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
}
});
},
downloadBlob: (filename, fileType, viewportElement, downloadCanvas) => {
const file = `${filename}.${fileType}`;
const mimetype = `image/${fileType}`;
/* Handles JPEG images for IE11 */
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
const image = downloadCanvas.toDataURL(mimetype, 1);
const blob = utils.b64toBlob(
image.replace('data:image/jpeg;base64,', ''),
mimetype
);
return window.navigator.msSaveBlob(blob, file);
}
viewportElement.querySelector('canvas').toBlob(blob => {
const URLObj = window.URL || window.webkitURL;
const a = document.createElement('a');
a.href = URLObj.createObjectURL(blob);
a.download = file;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
},
};
};
const ConnectedViewportDownloadForm = connect(
mapStateToProps,
null
)(ViewportDownloadForm);
export default ConnectedViewportDownloadForm;

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import { MODULE_TYPES } from '@ohif/core';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { MODULE_TYPES } from '@ohif/core';
import {
ExpandableToolMenu,
RoundedButtonGroup,
@ -14,7 +15,6 @@ import './ToolbarRow.css';
import { commandsManager, extensionManager } from './../App.js';
import ConnectedCineDialog from './ConnectedCineDialog';
import ConnectedViewportDownloadForm from './ConnectedViewportDownloadForm';
import ConnectedLayoutButton from './ConnectedLayoutButton';
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
@ -308,8 +308,7 @@ function _handleBuiltIn(button) {
}
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
modal.show({
content: ConnectedViewportDownloadForm,
commandsManager.runCommand('showDownloadViewportModal', {
title: t('Download High Quality Image'),
});
}