refactor(viewport): Switch to react-cornerstone-viewport
This commit is contained in:
parent
a42cc5f1a1
commit
943efb44e5
@ -6,11 +6,11 @@ Package.describe({
|
|||||||
|
|
||||||
Npm.depends({
|
Npm.depends({
|
||||||
hammerjs: '2.0.8',
|
hammerjs: '2.0.8',
|
||||||
'cornerstone-core': '2.2.7',
|
'cornerstone-core': '2.2.8',
|
||||||
'cornerstone-tools': '3.0.0-b.1421',
|
'cornerstone-tools': '3.0.0-b.1471',
|
||||||
'cornerstone-math': '0.1.6',
|
'cornerstone-math': '0.1.7',
|
||||||
'dicom-parser': '1.8.0',
|
'dicom-parser': '1.8.3',
|
||||||
'cornerstone-wado-image-loader': '2.1.4',
|
'cornerstone-wado-image-loader': '2.2.3',
|
||||||
'dcmjs': '0.2.1'
|
'dcmjs': '0.2.1'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,751 +0,0 @@
|
|||||||
import {
|
|
||||||
Component,
|
|
||||||
} from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
// import * as cornerstone from 'cornerstone-core';
|
|
||||||
// import * as cornerstoneTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
import {
|
|
||||||
cornerstone,
|
|
||||||
cornerstoneTools,
|
|
||||||
} from 'meteor/ohif:cornerstone';
|
|
||||||
import './CornerstoneViewport.styl';
|
|
||||||
// import './lib/initCornerstone.js';
|
|
||||||
import ImageScrollbar from '../ImageScrollbar/ImageScrollbar.js';
|
|
||||||
import ViewportOverlay from '../ViewportOverlay/ViewportOverlay.js';
|
|
||||||
import ToolContextMenu from '../ToolContextMenu/ToolContextMenu.js';
|
|
||||||
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator.js';
|
|
||||||
import ViewportOrientationMarkers from '../ViewportOrientationMarkers/ViewportOrientationMarkers.js';
|
|
||||||
import {
|
|
||||||
StackManager,
|
|
||||||
} from '../../../lib/StackManager';
|
|
||||||
import './CornerstoneViewport.styl';
|
|
||||||
|
|
||||||
const EVENT_RESIZE = 'resize';
|
|
||||||
const loadIndicatorDelay = 45;
|
|
||||||
const {
|
|
||||||
loadHandlerManager,
|
|
||||||
} = cornerstoneTools;
|
|
||||||
|
|
||||||
function setToolsPassive(tools) {
|
|
||||||
tools.forEach((tool) => {
|
|
||||||
cornerstoneTools.setToolPassive(tool);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeTools(tools) {
|
|
||||||
Array.from(tools).forEach((tool) => {
|
|
||||||
const apiTool = cornerstoneTools[`${tool.name}Tool`];
|
|
||||||
if (apiTool) {
|
|
||||||
cornerstoneTools.addTool(apiTool, tool.configuration);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Tool not found: ${tool.name}Tool`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCornerstoneStack(viewportData) {
|
|
||||||
const {
|
|
||||||
displaySetInstanceUid,
|
|
||||||
studyInstanceUid,
|
|
||||||
} = viewportData;
|
|
||||||
|
|
||||||
// Create shortcut to displaySet
|
|
||||||
const study = OHIF.viewer.Studies.findBy({
|
|
||||||
studyInstanceUid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const displaySet = study.displaySets.find((set) => {
|
|
||||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get stack from Stack Manager
|
|
||||||
const stack = StackManager.findOrCreateStack(study, displaySet);
|
|
||||||
stack.currentImageIdIndex = 0;
|
|
||||||
|
|
||||||
return stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const scrollToIndex = cornerstoneTools.import('util/scrollToIndex');
|
|
||||||
|
|
||||||
class CornerstoneViewport extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const stack = getCornerstoneStack(this.props.viewportData);
|
|
||||||
|
|
||||||
// TODO: Allow viewport as a prop
|
|
||||||
this.state = {
|
|
||||||
stack,
|
|
||||||
displaySetInstanceUid: this.props.viewportData.displaySetInstanceUid,
|
|
||||||
imageId: stack.imageIds[0],
|
|
||||||
viewportHeight: '100%',
|
|
||||||
isLoading: false, // true,
|
|
||||||
imageScrollbarValue: 0,
|
|
||||||
numImagesLoaded: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.displayScrollbar = stack.imageIds.length > 1;
|
|
||||||
this.state.viewport = cornerstone.getDefaultViewport(null, undefined);
|
|
||||||
|
|
||||||
this.onImageRendered = this.onImageRendered.bind(this);
|
|
||||||
this.onNewImage = this.onNewImage.bind(this);
|
|
||||||
this.onWindowResize = this.onWindowResize.bind(this);
|
|
||||||
this.onImageLoaded = this.onImageLoaded.bind(this);
|
|
||||||
this.onStackScroll = this.onStackScroll.bind(this);
|
|
||||||
this.startLoadingHandler = this.startLoadingHandler.bind(this);
|
|
||||||
this.doneLoadingHandler = this.doneLoadingHandler.bind(this);
|
|
||||||
this.onMouseClick = this.onMouseClick.bind(this);
|
|
||||||
this.onTouchPress = this.onTouchPress.bind(this);
|
|
||||||
this.onTouchStart = this.onTouchStart.bind(this);
|
|
||||||
this.onMeasurementAddedOrRemoved = this.onMeasurementAddedOrRemoved.bind(
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setViewportActive = this.setViewportActive.bind(this);
|
|
||||||
|
|
||||||
this.onCloseToolContextMenu = this.onCloseToolContextMenu.bind(this);
|
|
||||||
this.imageSliderOnInputCallback = this.imageSliderOnInputCallback.bind(
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.loadHandlerTimeout = 25;
|
|
||||||
// loadHandlerManager.setStartLoadHandler(this.startLoadingHandler);
|
|
||||||
// loadHandlerManager.setEndLoadHandler(this.doneLoadingHandler);
|
|
||||||
|
|
||||||
this.debouncedResize = debounce(() => {
|
|
||||||
try {
|
|
||||||
cornerstone.getEnabledElement(this.element);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerstone.resize(this.element, true);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
viewportHeight: `${this.element.clientHeight - 20}px`,
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
this.slideTimeoutTime = 25;
|
|
||||||
this.slideTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
activeTool: 'Wwwc',
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const isLoading = this.state.isLoading || this.state.numImagesLoaded / this.state.stack.imageIds.length < 0.1;
|
|
||||||
|
|
||||||
return (<div className="CornerstoneViewport" >
|
|
||||||
<ToolContextMenu
|
|
||||||
toolContextMenuData={
|
|
||||||
this.state.toolContextMenuData
|
|
||||||
}
|
|
||||||
onClose={
|
|
||||||
this.onCloseToolContextMenu
|
|
||||||
}
|
|
||||||
/> <div
|
|
||||||
className="viewport-element"
|
|
||||||
onContextMenu={
|
|
||||||
this.onContextMenu
|
|
||||||
}
|
|
||||||
ref={
|
|
||||||
(input) => {
|
|
||||||
this.element = input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isLoading ? <LoadingIndicator /> : ''
|
|
||||||
} <canvas className="cornerstone-canvas" />
|
|
||||||
<ViewportOverlay
|
|
||||||
stack={
|
|
||||||
this.state.stack
|
|
||||||
}
|
|
||||||
viewport={
|
|
||||||
this.state.viewport
|
|
||||||
}
|
|
||||||
imageId={
|
|
||||||
this.state.imageId
|
|
||||||
}
|
|
||||||
numImagesLoaded={
|
|
||||||
this.state.numImagesLoaded
|
|
||||||
}
|
|
||||||
/> <ViewportOrientationMarkers
|
|
||||||
imageId={
|
|
||||||
this.state.imageId
|
|
||||||
}
|
|
||||||
viewport={
|
|
||||||
this.state.viewport
|
|
||||||
}
|
|
||||||
/> </div > {
|
|
||||||
this.displayScrollbar && (<ImageScrollbar
|
|
||||||
onInputCallback={
|
|
||||||
this.imageSliderOnInputCallback
|
|
||||||
}
|
|
||||||
max={
|
|
||||||
this.state.stack.imageIds.length - 1
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
this.state.imageScrollbarValue
|
|
||||||
}
|
|
||||||
height={
|
|
||||||
this.state.viewportHeight
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} {
|
|
||||||
/* this.state.bidirectionalAddLabelShow && (
|
|
||||||
<Labelling
|
|
||||||
measurementData={this.bidirectional.measurementData}
|
|
||||||
eventData={this.bidirectional.eventData}
|
|
||||||
labellingDoneCallback={this.bidirectional.labellingDoneCallback}
|
|
||||||
skipButton={this.bidirectional.skipButton}
|
|
||||||
editDescription={this.bidirectional.editDescription}
|
|
||||||
/>
|
|
||||||
)*/
|
|
||||||
} </div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bidirectionalToolLabellingCallback = (
|
|
||||||
measurementData,
|
|
||||||
eventData,
|
|
||||||
doneCallback,
|
|
||||||
options = {},
|
|
||||||
) => {
|
|
||||||
const labellingDoneCallback = () => {
|
|
||||||
this.hideExtraButtons();
|
|
||||||
return doneCallback();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bidirectional = {
|
|
||||||
measurementData,
|
|
||||||
eventData,
|
|
||||||
labellingDoneCallback,
|
|
||||||
skipButton: options.skipButton,
|
|
||||||
editDescription: options.editDescription,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
bidirectionalAddLabelShow: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onContextMenu(event) {
|
|
||||||
// Preventing the default behaviour for right-click is essential to
|
|
||||||
// allow right-click tools to work.
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
onWindowResize() {
|
|
||||||
this.debouncedResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
onImageRendered() {
|
|
||||||
const viewport = cornerstone.getViewport(this.element);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
viewport,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onNewImage() {
|
|
||||||
const image = cornerstone.getImage(this.element);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
imageId: image.imageId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const element = this.element;
|
|
||||||
|
|
||||||
// Enable the DOM Element for use with Cornerstone
|
|
||||||
cornerstone.enable(element);
|
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
cornerstone.EVENTS.IMAGE_LOADED,
|
|
||||||
this.onImageLoaded,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load the first image in the stack
|
|
||||||
cornerstone.loadAndCacheImage(this.state.imageId).then((image) => {
|
|
||||||
try {
|
|
||||||
cornerstone.getEnabledElement(element);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle cases where the user ends the session before the image is displayed.
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Soft Tissue preset for all images by default
|
|
||||||
const viewport = cornerstone.getDefaultViewportForImage(element, image);
|
|
||||||
viewport.voi = {
|
|
||||||
windowWidth: 400,
|
|
||||||
windowCenter: 40,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display the first image
|
|
||||||
cornerstone.displayImage(element, image, viewport);
|
|
||||||
|
|
||||||
// Clear any previous tool state
|
|
||||||
cornerstoneTools.clearToolState(this.element, 'stack');
|
|
||||||
|
|
||||||
// Disable stack prefetch in case there are still queued requests
|
|
||||||
cornerstoneTools.stackPrefetch.disable(this.element);
|
|
||||||
|
|
||||||
// Add the stack tool state to the enabled element
|
|
||||||
const stack = this.state.stack;
|
|
||||||
cornerstoneTools.addStackStateManager(element, ['stack']);
|
|
||||||
cornerstoneTools.addToolState(element, 'stack', stack);
|
|
||||||
cornerstoneTools.stackPrefetch.enable(this.element);
|
|
||||||
|
|
||||||
const tools = [{
|
|
||||||
name: 'Bidirectional',
|
|
||||||
configuration: {
|
|
||||||
getMeasurementLocationCallback: this
|
|
||||||
.bidirectionalToolLabellingCallback,
|
|
||||||
shadow: true,
|
|
||||||
drawHandlesOnHover: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wwwc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Zoom',
|
|
||||||
configuration: {
|
|
||||||
minScale: 0.3,
|
|
||||||
maxScale: 25,
|
|
||||||
preventZoomOutsideImage: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Pan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StackScroll',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PanMultiTouch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ZoomTouchPinch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StackScrollMouseWheel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StackScrollMultiTouch',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
initializeTools(tools);
|
|
||||||
|
|
||||||
this.setActiveTool(this.props.activeTool);
|
|
||||||
|
|
||||||
/* For touch devices, by default we activate:
|
|
||||||
- Pinch to zoom
|
|
||||||
- Two-finger Pan
|
|
||||||
- Three (or more) finger Stack Scroll
|
|
||||||
*/
|
|
||||||
cornerstoneTools.setToolActive('PanMultiTouch', {
|
|
||||||
mouseButtonMask: 0,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
cornerstoneTools.setToolActive('ZoomTouchPinch', {
|
|
||||||
mouseButtonMask: 0,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
cornerstoneTools.setToolActive('StackScrollMultiTouch', {
|
|
||||||
mouseButtonMask: 0,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
|
||||||
maxImagesToPrefetch: Infinity,
|
|
||||||
preserveExistingPool: false,
|
|
||||||
maxSimultaneousRequests: 6,
|
|
||||||
});
|
|
||||||
|
|
||||||
/* For mouse devices, by default we turn on:
|
|
||||||
- Stack scrolling by mouse wheel
|
|
||||||
- Stack scrolling by keyboard up / down arrow keys
|
|
||||||
- Pan with middle click
|
|
||||||
- Zoom with right click
|
|
||||||
*/
|
|
||||||
|
|
||||||
cornerstoneTools.setToolActive('StackScrollMouseWheel', {
|
|
||||||
mouseButtonMask: 0,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstone.EVENTS.IMAGE_RENDERED,
|
|
||||||
this.onImageRendered,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, this.onNewImage);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.STACK_SCROLL,
|
|
||||||
this.onStackScroll,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
this.onMeasurementAddedOrRemoved,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MEASUREMENT_REMOVED,
|
|
||||||
this.onMeasurementAddedOrRemoved,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MOUSE_CLICK,
|
|
||||||
this.onMouseClick,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.TOUCH_PRESS,
|
|
||||||
this.onTouchPress,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cornerstoneTools.EVENTS.TOUCH_START,
|
|
||||||
this.onTouchStart,
|
|
||||||
);
|
|
||||||
|
|
||||||
window.addEventListener(EVENT_RESIZE, this.onWindowResize);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
viewportHeight: `${this.element.clientHeight - 20}px`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const element = this.element;
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstone.EVENTS.IMAGE_RENDERED,
|
|
||||||
this.onImageRendered,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, this.onNewImage);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.STACK_SCROLL,
|
|
||||||
this.onStackScroll,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
this.onMeasurementAddedOrRemoved,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MEASUREMENT_REMOVED,
|
|
||||||
this.onMeasurementAddedOrRemoved,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.MOUSE_CLICK,
|
|
||||||
this.onMouseClick,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.TOUCH_PRESS,
|
|
||||||
this.onTouchPress,
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cornerstoneTools.EVENTS.TOUCH_START,
|
|
||||||
this.onTouchStart,
|
|
||||||
);
|
|
||||||
|
|
||||||
window.removeEventListener(EVENT_RESIZE, this.onWindowResize);
|
|
||||||
|
|
||||||
cornerstone.disable(element);
|
|
||||||
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
cornerstone.EVENTS.IMAGE_LOADED,
|
|
||||||
this.onImageLoaded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
// TODO: Add a real object shallow comparison here?
|
|
||||||
if (
|
|
||||||
this.state.displaySetInstanceUid !== this.props.viewportData.displaySetInstanceUid
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
displaySetInstanceUid,
|
|
||||||
studyInstanceUid,
|
|
||||||
currentImageIdIndex,
|
|
||||||
} = this.props.viewportData;
|
|
||||||
|
|
||||||
// Create shortcut to displaySet
|
|
||||||
const study = OHIF.viewer.Studies.findBy({
|
|
||||||
studyInstanceUid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const displaySet = study.displaySets.find((set) => {
|
|
||||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get stack from Stack Manager
|
|
||||||
const stack = StackManager.findOrCreateStack(study, displaySet);
|
|
||||||
const stackData = cornerstoneTools.getToolState(this.element, 'stack');
|
|
||||||
let currentStack = stackData && stackData.data[0];
|
|
||||||
|
|
||||||
if (!currentStack) {
|
|
||||||
currentStack = {
|
|
||||||
currentImageIdIndex,
|
|
||||||
imageIds: stack.imageIds,
|
|
||||||
};
|
|
||||||
|
|
||||||
cornerstoneTools.addStackStateManager(this.element, ['stack']);
|
|
||||||
cornerstoneTools.addToolState(this.element, 'stack', currentStack);
|
|
||||||
} else {
|
|
||||||
// TODO: we should make something like setToolState by an ID
|
|
||||||
currentStack.currentImageIdIndex = currentImageIdIndex;
|
|
||||||
currentStack.imageIds = stack.imageIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageId = currentStack.imageIds[currentImageIdIndex];
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
displaySetInstanceUid,
|
|
||||||
studyInstanceUid,
|
|
||||||
stack,
|
|
||||||
imageId,
|
|
||||||
});
|
|
||||||
|
|
||||||
cornerstoneTools.stackPrefetch.disable(this.element);
|
|
||||||
cornerstone.loadAndCacheImage(imageId).then((image) => {
|
|
||||||
try {
|
|
||||||
cornerstone.getEnabledElement(this.element);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle cases where the user ends the session before the image is displayed.
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstone.getDefaultViewportForImage(this.element, image);
|
|
||||||
|
|
||||||
// Workaround for Cornerstone issue #304
|
|
||||||
viewport.displayedArea.brhc = {
|
|
||||||
x: image.width,
|
|
||||||
y: image.height,
|
|
||||||
};
|
|
||||||
|
|
||||||
cornerstone.displayImage(this.element, image, viewport);
|
|
||||||
|
|
||||||
cornerstoneTools.stackPrefetch.enable(this.element);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.activeTool !== prevProps.activeTool) {
|
|
||||||
this.setActiveTool(this.props.activeTool);
|
|
||||||
|
|
||||||
// TODO: Why do we need to do this in v3?
|
|
||||||
cornerstoneTools.setToolActive('StackScrollMouseWheel', {
|
|
||||||
mouseButtonMask: 0,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check this, causes infinite loop
|
|
||||||
// this.debouncedResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveTool = (activeTool) => {
|
|
||||||
const leftMouseTools = ['Bidirectional', 'Wwwc', 'StackScroll'];
|
|
||||||
|
|
||||||
setToolsPassive(leftMouseTools);
|
|
||||||
|
|
||||||
// pan is the default tool for middle mouse button
|
|
||||||
const isPanToolActive = activeTool === 'Pan';
|
|
||||||
const panOptions = {
|
|
||||||
mouseButtonMask: isPanToolActive ? [1, 4] : [4],
|
|
||||||
isTouchActive: isPanToolActive,
|
|
||||||
};
|
|
||||||
cornerstoneTools.setToolActive('Pan', panOptions);
|
|
||||||
|
|
||||||
// zoom is the default tool for right mouse button
|
|
||||||
const isZoomToolActive = activeTool === 'Zoom';
|
|
||||||
const zoomOptions = {
|
|
||||||
mouseButtonMask: isZoomToolActive ? [1, 2] : [2],
|
|
||||||
isTouchActive: isZoomToolActive,
|
|
||||||
};
|
|
||||||
cornerstoneTools.setToolActive('Zoom', zoomOptions);
|
|
||||||
|
|
||||||
cornerstoneTools.setToolActive(activeTool, {
|
|
||||||
mouseButtonMask: 1,
|
|
||||||
isTouchActive: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onStackScroll(event) {
|
|
||||||
this.setViewportActive();
|
|
||||||
|
|
||||||
const element = event.currentTarget;
|
|
||||||
const stackData = cornerstoneTools.getToolState(element, 'stack');
|
|
||||||
const stack = stackData.data[0];
|
|
||||||
|
|
||||||
this.hideExtraButtons();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
stack,
|
|
||||||
imageScrollbarValue: stack.currentImageIdIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onImageLoaded() {
|
|
||||||
this.setState({
|
|
||||||
numImagesLoaded: this.state.numImagesLoaded + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startLoadingHandler() {
|
|
||||||
// console.log('startLoadingHandler');
|
|
||||||
clearTimeout(this.loadHandlerTimeout);
|
|
||||||
this.loadHandlerTimeout = setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
isLoading: true,
|
|
||||||
});
|
|
||||||
}, loadIndicatorDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
doneLoadingHandler() {
|
|
||||||
clearTimeout(this.loadHandlerTimeout);
|
|
||||||
this.setState({
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMeasurementAddedOrRemoved() {
|
|
||||||
console.log('onMeasurementAddedOrRemoved');
|
|
||||||
/* const { toolType, measurementData } = event.detail;
|
|
||||||
|
|
||||||
// TODO: Pass in as prop?
|
|
||||||
const toolsOfInterest = ['Bidirectional'];
|
|
||||||
|
|
||||||
this.hideExtraButtons();
|
|
||||||
|
|
||||||
if (toolsOfInterest.includes(toolType)) {
|
|
||||||
const image = cornerstone.getImage(this.element);
|
|
||||||
const viewport = cornerstone.getViewport(this.element);
|
|
||||||
|
|
||||||
const type = {
|
|
||||||
cornerstonetoolsmeasurementadded: 'added',
|
|
||||||
cornerstonetoolsmeasurementremoved: 'removed'
|
|
||||||
};
|
|
||||||
const action = type[event.type];
|
|
||||||
|
|
||||||
if (action === 'added') {
|
|
||||||
measurementData._id = guid();
|
|
||||||
measurementData.viewport = cloneDeep(viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.measurementsAddedOrRemoved(
|
|
||||||
action,
|
|
||||||
image.imageId,
|
|
||||||
toolType,
|
|
||||||
measurementData
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewportActive() {
|
|
||||||
const {
|
|
||||||
viewportIndex,
|
|
||||||
} = this.props.viewportData;
|
|
||||||
const activeViewportIndex = window.store.getState().viewports.activeViewport;
|
|
||||||
if (viewportIndex !== activeViewportIndex) {
|
|
||||||
window.store.dispatch({
|
|
||||||
type: 'SET_VIEWPORT_ACTIVE',
|
|
||||||
viewportIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseClick(event) {
|
|
||||||
this.setViewportActive();
|
|
||||||
|
|
||||||
if (event.detail.event.which === 3) {
|
|
||||||
this.setState({
|
|
||||||
toolContextMenuData: {
|
|
||||||
eventData: event.detail,
|
|
||||||
isTouchEvent: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onTouchPress(event) {
|
|
||||||
this.setViewportActive();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
toolContextMenuData: {
|
|
||||||
eventData: event.detail,
|
|
||||||
isTouchEvent: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTouchStart() {
|
|
||||||
this.setViewportActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloseToolContextMenu() {
|
|
||||||
this.setState({
|
|
||||||
toolContextMenuData: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
imageSliderOnInputCallback(value) {
|
|
||||||
this.setViewportActive();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
imageScrollbarValue: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note that we throttle requests to prevent the
|
|
||||||
// user's ultrafast scrolling from firing requests too quickly.
|
|
||||||
// clearTimeout(this.slideTimeout);
|
|
||||||
// this.slideTimeout = setTimeout(() => {
|
|
||||||
scrollToIndex(this.element, value);
|
|
||||||
// }, this.slideTimeoutTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideExtraButtons = () => {
|
|
||||||
if (this.state.bidirectionalAddLabelShow === true) {
|
|
||||||
this.setState({
|
|
||||||
bidirectionalAddLabelShow: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
toolContextMenuData: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
CornerstoneViewport.propTypes = {
|
|
||||||
measurementsAddedOrRemoved: PropTypes.func,
|
|
||||||
measurementsChanged: PropTypes.func,
|
|
||||||
activeTool: PropTypes.string,
|
|
||||||
viewportData: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CornerstoneViewport;
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
@import "{ohif:design}/app"
|
|
||||||
|
|
||||||
.CornerstoneViewport
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
|
|
||||||
.viewport-element
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
position: relative;
|
|
||||||
background-color: black
|
|
||||||
|
|
||||||
// Prevent the blue outline in Chrome when a viewport is selected
|
|
||||||
outline: 0 !important
|
|
||||||
|
|
||||||
// Prevents the entire page from getting larger
|
|
||||||
// when the magnify tool is near the sides/corners of the page
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.viewportInstructions
|
|
||||||
display: block
|
|
||||||
font-size: 13px
|
|
||||||
theme('color', '$textSecondaryColor')
|
|
||||||
line-height: 18px
|
|
||||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
|
||||||
text-align: center
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
bottom: 0
|
|
||||||
right: 0
|
|
||||||
left: 0
|
|
||||||
margin: auto
|
|
||||||
height: 20px
|
|
||||||
|
|
||||||
/* These are some good CSS settings for a circular magnifying glass
|
|
||||||
Try removing the border-radius to make a square magnifying glass */
|
|
||||||
.magnifyTool
|
|
||||||
border: 4px white solid
|
|
||||||
box-shadow: 2px 2px 10px #1e1e1e
|
|
||||||
border-radius: 50%
|
|
||||||
display: none
|
|
||||||
cursor: none
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<template name="imageViewerViewport">
|
|
||||||
<div class='imageViewerViewport'
|
|
||||||
oncontextmenu='return false;'
|
|
||||||
unselectable='on'
|
|
||||||
onselectstart='return false;'>
|
|
||||||
</div>
|
|
||||||
<div class='viewportInstructions'>
|
|
||||||
Please drag a stack here to view images.
|
|
||||||
</div>
|
|
||||||
{{>viewportOverlay}}
|
|
||||||
{{>loadingIndicator}}
|
|
||||||
{{>viewportOrientationMarkers}}
|
|
||||||
</template>
|
|
||||||
@ -1,697 +0,0 @@
|
|||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { Template } from 'meteor/templating';
|
|
||||||
import { Tracker } from 'meteor/tracker';
|
|
||||||
import { Session } from 'meteor/session';
|
|
||||||
import { $ } from 'meteor/jquery';
|
|
||||||
// OHIF Modules
|
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
|
|
||||||
// Local Modules
|
|
||||||
import { StackManager } from '../../../lib/StackManager';
|
|
||||||
import { setActiveViewport } from '../../../lib/setActiveViewport';
|
|
||||||
import { imageViewerViewportData } from '../../../lib/imageViewerViewportData';
|
|
||||||
import { updateCrosshairsSynchronizer } from '../../../lib/updateCrosshairsSynchronizer';
|
|
||||||
import { toolManager } from '../../../lib/toolManager';
|
|
||||||
import { updateOrientationMarkers } from '../../../lib/updateOrientationMarkers';
|
|
||||||
import { getInstanceClassDefaultViewport } from '../../../lib/instanceClassSpecificViewport';
|
|
||||||
import { OHIFError } from '../../../lib/classes/OHIFError';
|
|
||||||
|
|
||||||
const allCornerstoneEvents = ['click', 'cornerstonetoolsmousedown', 'cornerstonetoolsmousedownactivate',
|
|
||||||
'cornerstonetoolsmouseclick', 'cornerstonetoolsmousedrag', 'cornerstonetoolsmouseup',
|
|
||||||
'cornerstonetoolsmousewheel', 'cornerstonetoolsdoubletap', 'cornerstonetoolstouchpress',
|
|
||||||
'cornerstonetoolsmultitouchstart', 'cornerstonetoolsmultitouchstartactive', 'cornerstonetoolsmultitouchdrag'];
|
|
||||||
|
|
||||||
const PLUGIN_CORNERSTONE = 'cornerstone';
|
|
||||||
|
|
||||||
// Create a way to add hooks to be executed every time a cornerstone element is enabled
|
|
||||||
OHIF.viewer.cornerstoneElementHooks = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function loads a study series into a viewport element.
|
|
||||||
*
|
|
||||||
* @param data {object} Object containing the study, series, and viewport element to be used
|
|
||||||
*/
|
|
||||||
const loadDisplaySetIntoViewport = (data, templateData) => {
|
|
||||||
const wlPresets = OHIF.viewerbase.wlPresets;
|
|
||||||
|
|
||||||
OHIF.log.info('imageViewerViewport loadDisplaySetIntoViewport');
|
|
||||||
|
|
||||||
// Make sure we have all the data required to render the series
|
|
||||||
if (!data.study || !data.displaySet || !data.element) {
|
|
||||||
OHIF.log.warn('loadDisplaySetIntoViewport: No Study, Display Set, or Element provided');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current element and it's index in the list of all viewports
|
|
||||||
// The viewport index is often used to store information about a viewport element
|
|
||||||
const element = data.element;
|
|
||||||
const viewportIndex = templateData.viewportIndex;
|
|
||||||
|
|
||||||
const layoutManager = OHIF.viewerbase.layoutManager;
|
|
||||||
layoutManager.viewportData = layoutManager.viewportData || {};
|
|
||||||
layoutManager.viewportData[viewportIndex] = layoutManager.viewportData[viewportIndex] || {};
|
|
||||||
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
|
|
||||||
|
|
||||||
// Stop here if no data was defined for the viewer
|
|
||||||
if (!OHIF.viewer.data) return;
|
|
||||||
|
|
||||||
// This data will be saved so that the viewport can be reloaded to the same state later
|
|
||||||
OHIF.viewer.data.loadedSeriesData[viewportIndex] = {};
|
|
||||||
|
|
||||||
// Create shortcut to displaySet
|
|
||||||
const displaySet = data.displaySet;
|
|
||||||
|
|
||||||
// Get stack from Stack Manager
|
|
||||||
let stack = StackManager.findOrCreateStack(data.study, displaySet);
|
|
||||||
|
|
||||||
// If is a clip, updates the global FPS for cine dialog
|
|
||||||
if (stack.isClip && stack.frameRate > 0) {
|
|
||||||
// Sets the global variable
|
|
||||||
OHIF.viewer.cine.framesPerSecond = parseFloat(stack.frameRate);
|
|
||||||
// Update the cine dialog FPS
|
|
||||||
Session.set('UpdateCINE', Math.random());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shortcut for array with image IDs
|
|
||||||
const imageIds = stack.imageIds;
|
|
||||||
|
|
||||||
// Define the current image stack using the newly created image IDs
|
|
||||||
stack = {
|
|
||||||
currentImageIdIndex: data.currentImageIdIndex > 0 && data.currentImageIdIndex < imageIds.length ? data.currentImageIdIndex : 0,
|
|
||||||
imageIds: imageIds,
|
|
||||||
displaySetInstanceUid: data.displaySetInstanceUid
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the current image ID for the stack that will be rendered
|
|
||||||
const imageId = imageIds[stack.currentImageIdIndex];
|
|
||||||
|
|
||||||
// Save the current image ID inside the template data so it can be
|
|
||||||
// retrieved from the template helpers
|
|
||||||
templateData.imageId = imageId;
|
|
||||||
|
|
||||||
// Save the current image ID inside the ViewportLoading object.
|
|
||||||
//
|
|
||||||
// The ViewportLoading object relates the viewport elements with whichever
|
|
||||||
// image is currently being loaded into them. This is useful so that we can
|
|
||||||
// place progress (download %) for each image inside the proper viewports.
|
|
||||||
window.ViewportLoading[viewportIndex] = imageId;
|
|
||||||
|
|
||||||
// Enable Cornerstone for the viewport element
|
|
||||||
const options = {
|
|
||||||
renderer: OHIF.cornerstone.renderer
|
|
||||||
};
|
|
||||||
cornerstone.enable(element, options);
|
|
||||||
|
|
||||||
// Call every defined hook
|
|
||||||
OHIF.viewer.cornerstoneElementHooks.forEach(hook => {
|
|
||||||
if (typeof hook === 'function') {
|
|
||||||
hook(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the handler functions that will run when loading has finished or thrown
|
|
||||||
// an error. These are used to show/hide loading / error text boxes on each viewport.
|
|
||||||
const endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
|
|
||||||
const errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
|
|
||||||
|
|
||||||
// Get the current viewport settings
|
|
||||||
const viewport = cornerstone.getViewport(element);
|
|
||||||
|
|
||||||
const { studyInstanceUid, seriesInstanceUid, displaySetInstanceUid, currentImageIdIndex } = data;
|
|
||||||
|
|
||||||
// Store the current series data inside the Layout Manager
|
|
||||||
layoutManager.viewportData[viewportIndex] = {
|
|
||||||
imageId,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
currentImageIdIndex,
|
|
||||||
viewport: viewport || data.viewport,
|
|
||||||
viewportIndex,
|
|
||||||
plugin: PLUGIN_CORNERSTONE
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle the case where the imageId isn't loaded correctly and the
|
|
||||||
// imagePromise returns undefined
|
|
||||||
// To test, uncomment the next line
|
|
||||||
// data.imageId = 'AfileThatDoesntWork'; // For testing only!
|
|
||||||
|
|
||||||
let imagePromise;
|
|
||||||
try {
|
|
||||||
imagePromise = cornerstone.loadAndCacheImage(imageId);
|
|
||||||
} catch (error) {
|
|
||||||
OHIF.log.info(error);
|
|
||||||
if (!imagePromise) {
|
|
||||||
errorLoadingHandler(element, imageId, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional tasks for metadata provider. If using your own
|
|
||||||
// metadata provider, this may not be necessary.
|
|
||||||
// updateMetadata is important, though, to update image metadata that
|
|
||||||
// for any reason was missing some information such as rows, columns,
|
|
||||||
// sliceThickness, etc (See MetadataProvider class from ohif-cornerstone package)
|
|
||||||
const metadataProvider = OHIF.viewer.metadataProvider;
|
|
||||||
const isUpdateMetadataDefined = metadataProvider && typeof metadataProvider.updateMetadata === 'function';
|
|
||||||
|
|
||||||
// loadAndCacheImage configurable callbacks
|
|
||||||
const callbacks = imageViewerViewportData.callbacks;
|
|
||||||
|
|
||||||
// Check if it has before loadAndCacheImage callback
|
|
||||||
if (typeof callbacks.before === 'function') {
|
|
||||||
OHIF.log.info('imageViewerViewport before loadAndCacheImage callback');
|
|
||||||
callbacks.before(imagePromise, templateData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start loading the image.
|
|
||||||
imagePromise.then(image => {
|
|
||||||
let enabledElement;
|
|
||||||
try {
|
|
||||||
enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
OHIF.log.warn('Viewport destroyed before loaded image could be displayed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caches element's jQuery object
|
|
||||||
const $element = $(element);
|
|
||||||
|
|
||||||
// Update the enabled element with the image and viewport data
|
|
||||||
// This is not usually necessary, but we need them stored in case
|
|
||||||
// a sopClassUid-specific viewport setting is present.
|
|
||||||
enabledElement.image = image;
|
|
||||||
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, image);
|
|
||||||
|
|
||||||
if (isUpdateMetadataDefined) {
|
|
||||||
// Update the metaData for missing fields
|
|
||||||
metadataProvider.updateMetadata(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are default viewport settings for this sopClassUid
|
|
||||||
if (!displaySet.images || !displaySet.images.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = displaySet.images[0];
|
|
||||||
const instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, image.imageId);
|
|
||||||
|
|
||||||
// If there are sopClassUid-specific viewport settings, apply them
|
|
||||||
if (instanceClassViewport) {
|
|
||||||
cornerstone.displayImage(element, image, instanceClassViewport);
|
|
||||||
|
|
||||||
// Mark that this element should not be fit to the window in the resize listeners
|
|
||||||
// TODO: Find another way to do this?
|
|
||||||
enabledElement.fitToWindow = false;
|
|
||||||
|
|
||||||
// Resize the canvas to fit the current viewport element size.
|
|
||||||
cornerstone.resize(element, false);
|
|
||||||
} else if (data.viewport) {
|
|
||||||
// If there is a saved object containing Cornerstone viewport data
|
|
||||||
// (e.g. scale, invert, window settings) in the input data, apply it now.
|
|
||||||
cornerstone.displayImage(element, image, data.viewport);
|
|
||||||
|
|
||||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
|
||||||
// image to the canvas dimensions.
|
|
||||||
cornerstone.resize(element, true);
|
|
||||||
} else {
|
|
||||||
// If no saved viewport settings or modality-specific settings exists,
|
|
||||||
// display the loaded image in the viewport element with no loaded viewport
|
|
||||||
// settings.
|
|
||||||
cornerstone.displayImage(element, image);
|
|
||||||
|
|
||||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
|
||||||
// image to the canvas dimensions.
|
|
||||||
cornerstone.resize(element, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set/store W/L preset data to Default on first display
|
|
||||||
wlPresets.updateElementWLPresetData(element);
|
|
||||||
|
|
||||||
// Remove the data for this viewport from the ViewportLoading object
|
|
||||||
// This will stop the loading percentage complete from being displayed.
|
|
||||||
delete window.ViewportLoading[viewportIndex];
|
|
||||||
|
|
||||||
// Call the handler function that represents the end of the image loading phase
|
|
||||||
// (e.g. hide the progress text box)
|
|
||||||
endLoadingHandler(element, image);
|
|
||||||
|
|
||||||
// Remove the 'empty' class from the viewport to hide any instruction text
|
|
||||||
element.classList.remove('empty');
|
|
||||||
|
|
||||||
// Hide the viewport instructions (i.e. 'Drag a stack here') and show
|
|
||||||
// the viewport overlay data.
|
|
||||||
$element.siblings('.viewportInstructions').hide();
|
|
||||||
$element.siblings('.imageViewerViewportOverlay').show();
|
|
||||||
|
|
||||||
// Add stack state managers for the stack tool, CINE tool, and reference lines
|
|
||||||
cornerstoneTools.addStackStateManager(element, ['stack', 'playClip', 'referenceLines']);
|
|
||||||
|
|
||||||
// Enable orientation markers, if applicable
|
|
||||||
updateOrientationMarkers(element);
|
|
||||||
|
|
||||||
// Clear any old stack data
|
|
||||||
cornerstoneTools.clearToolState(element, 'stack');
|
|
||||||
cornerstoneTools.addToolState(element, 'stack', stack);
|
|
||||||
|
|
||||||
// Set the default CINE settings
|
|
||||||
const multiframeMetadata = instance.getDataProperty('multiframeMetadata');
|
|
||||||
|
|
||||||
let fps;
|
|
||||||
if (multiframeMetadata && multiframeMetadata.averageFrameRate > 0) {
|
|
||||||
fps = multiframeMetadata.averageFrameRate;
|
|
||||||
} else {
|
|
||||||
fps = OHIF.viewer.cine.framesPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cineToolData = {
|
|
||||||
loop: OHIF.viewer.cine.loop,
|
|
||||||
framesPerSecond: fps
|
|
||||||
};
|
|
||||||
|
|
||||||
cornerstoneTools.addToolState(element, 'playClip', cineToolData);
|
|
||||||
|
|
||||||
// Autoplay datasets that have framerates set
|
|
||||||
if (multiframeMetadata && multiframeMetadata.isMultiframeImage && multiframeMetadata.averageFrameRate > 0) {
|
|
||||||
cornerstoneTools.playClip(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instantiate the tools
|
|
||||||
toolManager.instantiateTools(element);
|
|
||||||
|
|
||||||
// Use the tool manager to enable the currently active tool for this
|
|
||||||
// newly rendered element
|
|
||||||
toolManager.setActiveTool();
|
|
||||||
|
|
||||||
// Define a function to run whenever the Cornerstone viewport is rendered
|
|
||||||
// (e.g. following a change of window or zoom)
|
|
||||||
const onImageRendered = (event) => {
|
|
||||||
const eventData = event.detail;
|
|
||||||
const { viewport, element } = eventData;
|
|
||||||
|
|
||||||
// Attention: Adding OHIF.log.info in this function may decrease the performance
|
|
||||||
// since this callback function is called multiple times (eg: when a tool is
|
|
||||||
// enabled/disabled -> cornerstone[toolName].tool.enable)
|
|
||||||
|
|
||||||
if (!layoutManager.viewportData[viewportIndex]) {
|
|
||||||
OHIF.log.warn(`onImageRendered: LayoutManager has no viewport data for this viewport index?: ${viewportIndex}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Session to trigger reactive updates in the viewportOverlay helper functions
|
|
||||||
// This lets the viewport overlay always display correct window / zoom values
|
|
||||||
Session.set('CornerstoneImageRendered' + viewportIndex, Math.random());
|
|
||||||
|
|
||||||
// Save the current viewport into the OHIF.viewer.data global variable
|
|
||||||
layoutManager.viewportData[viewportIndex].viewport = viewport;
|
|
||||||
OHIF.viewer.data.loadedSeriesData[viewportIndex].viewport = viewport;
|
|
||||||
|
|
||||||
// Update the W/L Preset data, if necessary
|
|
||||||
wlPresets.updateElementWLPresetData(element);
|
|
||||||
|
|
||||||
// Check if it has onImageRendered loadAndCacheImage callback
|
|
||||||
if (typeof callbacks.onImageRendered === 'function') {
|
|
||||||
callbacks.onImageRendered(event, eventData, viewportIndex, templateData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach the onImageRendered callback to the CornerstoneImageRendered event
|
|
||||||
element.removeEventListener('cornerstoneimagerendered', onImageRendered);
|
|
||||||
element.addEventListener('cornerstoneimagerendered', onImageRendered);
|
|
||||||
|
|
||||||
// Set a random value for the Session variable in order to trigger an overlay update
|
|
||||||
Session.set('CornerstoneImageRendered' + viewportIndex, Math.random());
|
|
||||||
|
|
||||||
// Define a function to run whenever the Cornerstone viewport changes images
|
|
||||||
// (e.g. during scrolling)
|
|
||||||
const onNewImage = (event) => {
|
|
||||||
const eventData = event.detail;
|
|
||||||
|
|
||||||
// Attention: Adding OHIF.log.info in this function may decrease the performance
|
|
||||||
// since this callback function is called multiple times (eg: when a tool is
|
|
||||||
// enabled/disabled -> cornerstone[toolName].tool.enable)
|
|
||||||
|
|
||||||
if (isUpdateMetadataDefined) {
|
|
||||||
// Update the metaData for missing fields
|
|
||||||
metadataProvider.updateMetadata(eventData.enabledElement.image);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the templateData with the new imageId
|
|
||||||
// This allows the template helpers to update reactively
|
|
||||||
templateData.imageId = eventData.enabledElement.image.imageId;
|
|
||||||
Session.set('CornerstoneNewImage' + viewportIndex, Math.random());
|
|
||||||
layoutManager.viewportData[viewportIndex].imageId = eventData.enabledElement.image.imageId;
|
|
||||||
|
|
||||||
// Get the element and stack data
|
|
||||||
const element = event.target;
|
|
||||||
const toolData = cornerstoneTools.getToolState(element, 'stack');
|
|
||||||
if (!toolData || !toolData.data || !toolData.data.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update orientation markers in case new slices don't have the same orientation
|
|
||||||
// as the first slice
|
|
||||||
updateOrientationMarkers(element);
|
|
||||||
|
|
||||||
// If this viewport is displaying a stack of images, save the current image
|
|
||||||
// index in the stack to the global OHIF.viewer.data object.
|
|
||||||
const stack = cornerstoneTools.getToolState(element, 'stack');
|
|
||||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
|
||||||
const imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
|
|
||||||
layoutManager.viewportData[viewportIndex].currentImageIdIndex = imageIdIndex;
|
|
||||||
OHIF.viewer.data.loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
|
|
||||||
const wlPresetDataName = wlPresetData && wlPresetData.name;
|
|
||||||
wlPresets.applyWLPreset(wlPresetDataName, element);
|
|
||||||
|
|
||||||
// Check if it has onNewImage loadAndCacheImage callback
|
|
||||||
if (typeof callbacks.onNewImage === 'function') {
|
|
||||||
callbacks.onNewImage(event, eventData, viewportIndex, templateData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach the onNewImage callback to the CornerstoneNewImage event
|
|
||||||
element.removeEventListener('cornerstonenewimage', onNewImage);
|
|
||||||
element.addEventListener('cornerstonenewimage', onNewImage);
|
|
||||||
|
|
||||||
// Set a random value for the Session variable in order to trigger an overlay update
|
|
||||||
Session.set('CornerstoneNewImage' + viewportIndex, Math.random());
|
|
||||||
|
|
||||||
const onStackScroll = () => {
|
|
||||||
// Attention: Adding OHIF.log.info in this function may decrease the performance
|
|
||||||
// since this callback function is called multiple times (eg: when a tool is
|
|
||||||
// enabled/disabled -> cornerstone[toolName].tool.enable)
|
|
||||||
|
|
||||||
// Update the imageSlider value
|
|
||||||
Session.set('CornerstoneNewImage' + viewportIndex, Math.random());
|
|
||||||
};
|
|
||||||
|
|
||||||
element.removeEventListener('cornerstonestackscroll', onStackScroll);
|
|
||||||
if (stack.imageIds.length > 1) {
|
|
||||||
element.addEventListener('cornerstonestackscroll', onStackScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a function to trigger an event whenever a new viewport is being used
|
|
||||||
// This is used to update the value of the "active viewport", when the user interacts
|
|
||||||
// with a new viewport element
|
|
||||||
const sendActivationTrigger = (event) => {
|
|
||||||
const eventData = event && event.detail;
|
|
||||||
// Attention: Adding OHIF.log.info in this function decrease the performance
|
|
||||||
// since this callback function is called multiple times (eg: when a tool is
|
|
||||||
// enabled/disabled -> cornerstone[toolName].tool.enable)
|
|
||||||
|
|
||||||
// Reset the focus, even if we don't need to re-enable reference lines or prefetching
|
|
||||||
const element = (eventData && eventData.element) || (event && event.currentTarget);
|
|
||||||
if (!element) return;
|
|
||||||
const $element = $(element);
|
|
||||||
|
|
||||||
// Stop here if we don't have eventData set
|
|
||||||
if (!eventData) return;
|
|
||||||
|
|
||||||
// Check if the current active viewport in the Meteor Session
|
|
||||||
// Is the same as the viewport in which the activation event was fired.
|
|
||||||
// If it was, no changes are necessary, so stop here.
|
|
||||||
const activeViewportIndex = window.store.getState().viewports.activeViewport;
|
|
||||||
if (viewportIndex === activeViewportIndex) return;
|
|
||||||
|
|
||||||
$element.focus();
|
|
||||||
|
|
||||||
OHIF.log.info('imageViewerViewport sendActivationTrigger');
|
|
||||||
|
|
||||||
// Otherwise, trigger an 'OHIFActivateViewport' event to be handled by the Template event
|
|
||||||
// handler
|
|
||||||
eventData.viewportIndex = viewportIndex;
|
|
||||||
const customEvent = $.Event('OHIFActivateViewport', eventData);
|
|
||||||
|
|
||||||
// Need to overwrite the type set in the original event
|
|
||||||
customEvent.type = 'OHIFActivateViewport';
|
|
||||||
$element.trigger(customEvent, eventData);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle mouseenter event to send viewport activation trigger only if there is no focused dropdown
|
|
||||||
const onMouseEnter = () => {
|
|
||||||
if ($(':focus').closest('.dropdown').length) return;
|
|
||||||
|
|
||||||
sendActivationTrigger();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
|
|
||||||
allCornerstoneEvents.forEach(eventType => {
|
|
||||||
element.removeEventListener(eventType, sendActivationTrigger);
|
|
||||||
element.addEventListener(eventType, sendActivationTrigger);
|
|
||||||
});
|
|
||||||
$element.off('mouseenter', onMouseEnter);
|
|
||||||
$element.on('mouseenter', onMouseEnter);
|
|
||||||
|
|
||||||
OHIF.viewer.data.loadedSeriesData = layoutManager.viewportData;
|
|
||||||
|
|
||||||
// Check if image plane (orientation / location) data is present for the current image
|
|
||||||
const imagePlane = cornerstone.metaData.get('imagePlane', image.imageId);
|
|
||||||
if (imagePlane && imagePlane.frameOfReferenceUID) {
|
|
||||||
// If it is, add this element to the global synchronizer...
|
|
||||||
OHIF.viewer.updateImageSynchronizer.add(element);
|
|
||||||
|
|
||||||
if (OHIF.viewer.refLinesEnabled) {
|
|
||||||
// ... and if reference lines are globally enabled, let cornerstoneTools know.
|
|
||||||
cornerstoneTools.setToolEnabledForElement(element, 'referenceLines', {
|
|
||||||
synchronizationContext: OHIF.viewer.updateImageSynchronizer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the crosshairs tool is active, update the synchronizer
|
|
||||||
// that is used for its synchronized viewport updating.
|
|
||||||
// This is necessary if this new image shares a frame of reference
|
|
||||||
// with currently displayed images
|
|
||||||
const activeTool = toolManager.getActiveTool();
|
|
||||||
if (activeTool === 'crosshairs') {
|
|
||||||
updateCrosshairsSynchronizer(imagePlane.frameOfReferenceUID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the active viewport based on the Session variable
|
|
||||||
// This is done to ensure that the active element has the current
|
|
||||||
// focus, so that keyboard events are triggered.
|
|
||||||
if (viewportIndex === window.store.getState().viewports.activeViewport) {
|
|
||||||
const viewportContainer = $element.parents('.viewportContainer');
|
|
||||||
|
|
||||||
setActiveViewport(viewportContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run any renderedCallback that exists in the data context
|
|
||||||
if (data.renderedCallback && typeof data.renderedCallback === 'function') {
|
|
||||||
data.renderedCallback(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it has after loadAndCacheImage callback
|
|
||||||
if (typeof callbacks.after === 'function') {
|
|
||||||
OHIF.log.info('imageViewerViewport after callback');
|
|
||||||
callbacks.after(image, templateData, element);
|
|
||||||
}
|
|
||||||
}, error => {
|
|
||||||
// If something goes wrong while loading the image, fire the error handler.
|
|
||||||
errorLoadingHandler(element, imageId, error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function sets the display set for the study and calls LoadDisplaySetIntoViewport function
|
|
||||||
*
|
|
||||||
* @param data includes study data
|
|
||||||
* @param displaySetInstanceUid Display set information which is loaded in Template
|
|
||||||
* @param templateData currentData of Template
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const setDisplaySet = (data, displaySetInstanceUid, templateData) => {
|
|
||||||
const study = data.study;
|
|
||||||
|
|
||||||
if (!study) {
|
|
||||||
throw new OHIFError('Study does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
let displaySets = study.displaySets;
|
|
||||||
if (!displaySets.length) {
|
|
||||||
displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(study);
|
|
||||||
study.displaySets = displaySets;
|
|
||||||
study.setDisplaySets(displaySets);
|
|
||||||
|
|
||||||
study.forEachDisplaySet(displaySet => {
|
|
||||||
OHIF.viewerbase.stackManager.makeAndAddStack(study, displaySet);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!displaySets) {
|
|
||||||
throw new OHIFError('Study has no display sets');
|
|
||||||
}
|
|
||||||
|
|
||||||
displaySets.every(displaySet => {
|
|
||||||
if (displaySet.displaySetInstanceUid === displaySetInstanceUid) {
|
|
||||||
data.displaySet = displaySet;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we didn't find anything, stop here
|
|
||||||
if (!data.displaySet) {
|
|
||||||
data.displaySet = displaySets[0];
|
|
||||||
// throw new OHIFError('Display set not found in specified study!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, load pass the data object into loadSeriesIntoViewport
|
|
||||||
loadDisplaySetIntoViewport(data, templateData);
|
|
||||||
};
|
|
||||||
|
|
||||||
Meteor.startup(() => {
|
|
||||||
window.ViewportLoading = window.ViewportLoading || {};
|
|
||||||
toolManager.configureLoadProcess();
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.imageViewerViewport.onRendered(function() {
|
|
||||||
const templateData = Template.currentData();
|
|
||||||
OHIF.log.info('imageViewerViewport onRendered');
|
|
||||||
|
|
||||||
// When the imageViewerViewport template is rendered
|
|
||||||
const element = this.find('.imageViewerViewport');
|
|
||||||
this.element = element;
|
|
||||||
this.$element = $(element);
|
|
||||||
|
|
||||||
// Display the loading indicator for this element
|
|
||||||
this.$element.siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
|
||||||
|
|
||||||
// Get the current active viewport index, if this viewport has the same index,
|
|
||||||
// add the CSS 'active' class to highlight this viewport.
|
|
||||||
const activeViewport = window.store.getState().viewports.activeViewport;
|
|
||||||
|
|
||||||
// Focus the viewport if it's the active one
|
|
||||||
if (templateData.viewportIndex === activeViewport) {
|
|
||||||
this.$element.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
let { currentImageIdIndex } = templateData;
|
|
||||||
const { viewport, studyInstanceUid, seriesInstanceUid, renderedCallback, displaySetInstanceUid } = templateData;
|
|
||||||
|
|
||||||
if (!currentImageIdIndex) {
|
|
||||||
currentImageIdIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls extendData function to provide flexibility between systems
|
|
||||||
imageViewerViewportData.extendData(templateData);
|
|
||||||
|
|
||||||
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
|
|
||||||
const data = {
|
|
||||||
element,
|
|
||||||
viewport,
|
|
||||||
currentImageIdIndex,
|
|
||||||
displaySetInstanceUid,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
renderedCallback,
|
|
||||||
activeViewport
|
|
||||||
};
|
|
||||||
|
|
||||||
// If no displaySetInstanceUid was supplied, display the drag/drop
|
|
||||||
// instructions and then stop here since we don't know what to display in the viewport.
|
|
||||||
if (!displaySetInstanceUid) {
|
|
||||||
element.classList.add('empty');
|
|
||||||
this.$element.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
|
||||||
this.$element.siblings('.viewportInstructions').show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TypeSafeStudies
|
|
||||||
const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
|
|
||||||
|
|
||||||
data.study = study;
|
|
||||||
setDisplaySet(data, displaySetInstanceUid, templateData);
|
|
||||||
|
|
||||||
// Double click event handlers to handle viewport enlargement
|
|
||||||
function doubleClickHandler (event) {
|
|
||||||
const $element = $(this);
|
|
||||||
const { layoutManager } = OHIF.viewerbase;
|
|
||||||
const $viewports = $('.imageViewerViewport');
|
|
||||||
|
|
||||||
$element.trigger('ohif.viewer.viewport.toggleEnlargement');
|
|
||||||
|
|
||||||
// Get the double clicked viewport index
|
|
||||||
const viewportIndex = $viewports.index(event.currentTarget);
|
|
||||||
|
|
||||||
// Stop here if there's only one viewport
|
|
||||||
if (!layoutManager.isZoomed && $viewports.length <= 1) return;
|
|
||||||
|
|
||||||
// Enlarge the double clicked viewport
|
|
||||||
layoutManager.toggleEnlargement(viewportIndex);
|
|
||||||
|
|
||||||
// Wait for DOM re-rendering and update the active viewport
|
|
||||||
Tracker.afterFlush(() => {
|
|
||||||
let viewportIndexToZoom;
|
|
||||||
// Check if the viewer is zoomed
|
|
||||||
if (layoutManager.isZoomed) {
|
|
||||||
// Set the active viewport as the only one visible
|
|
||||||
viewportIndexToZoom = 0;
|
|
||||||
} else {
|
|
||||||
// Set the active viewport as the previous zoomed viewport
|
|
||||||
viewportIndexToZoom = layoutManager.zoomedViewportIndex || 0;
|
|
||||||
}
|
|
||||||
// Set zoomed viewport as active...
|
|
||||||
const viewportContainer = $('.viewportContainer').get(viewportIndexToZoom);
|
|
||||||
setActiveViewport(viewportContainer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const doubleClickEvents = ['cornerstonetoolsmousedoubleclick', 'cornerstonetoolsdoubletap'];
|
|
||||||
doubleClickEvents.forEach(eventType => {
|
|
||||||
element.removeEventListener(eventType, doubleClickHandler);
|
|
||||||
element.addEventListener(eventType, doubleClickHandler);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.imageViewerViewport.onDestroyed(function() {
|
|
||||||
OHIF.log.info('imageViewerViewport onDestroyed');
|
|
||||||
|
|
||||||
// When a viewport element is being destroyed
|
|
||||||
const element = this.find('.imageViewerViewport');
|
|
||||||
const $element = $(element);
|
|
||||||
if (!element || $element.hasClass('empty') || !$element.find('canvas').length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all tools for the destroyed element
|
|
||||||
toolManager.removeToolsForElement(element);
|
|
||||||
|
|
||||||
OHIF.viewer.updateImageSynchronizer.remove(element);
|
|
||||||
|
|
||||||
// Clear the stack prefetch data
|
|
||||||
cornerstoneTools.clearToolState(element, 'stackPrefetch');
|
|
||||||
|
|
||||||
// Try to stop any currently playing clips
|
|
||||||
// Otherwise the interval will continuously throw errors
|
|
||||||
try {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
if (enabledElement) {
|
|
||||||
cornerstoneTools.stopClip(element);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
OHIF.log.warn(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger custom Destroy Viewport event
|
|
||||||
// for compatibility with other systems
|
|
||||||
$element.trigger('OHIFDestroyedViewport');
|
|
||||||
|
|
||||||
// Disable the viewport element with Cornerstone
|
|
||||||
// This also triggers the removal of the element from all available
|
|
||||||
// synchronizers, such as the one used for reference lines.
|
|
||||||
cornerstone.disable(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.imageViewerViewport.events({
|
|
||||||
'OHIFActivateViewport .imageViewerViewport'(event) {
|
|
||||||
OHIF.log.info('imageViewerViewport OHIFActivateViewport');
|
|
||||||
|
|
||||||
const viewportContainer = $(event.currentTarget).parents('.viewportContainer').get(0);
|
|
||||||
setActiveViewport(viewportContainer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -2,13 +2,37 @@ import { Component } from 'react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
import CornerstoneViewport from '../CornerstoneViewport/CornerstoneViewport.js';
|
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||||
import './GridLayout.styl';
|
import './GridLayout.styl';
|
||||||
|
import StackManager from '../../../lib/StackManager.js';
|
||||||
|
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
|
||||||
|
|
||||||
const TOP_CLASS = 'top';
|
const TOP_CLASS = 'top';
|
||||||
const BOTTOM_CLASS = 'bottom';
|
const BOTTOM_CLASS = 'bottom';
|
||||||
const MIDDLE_CLASS = 'middle';
|
const MIDDLE_CLASS = 'middle';
|
||||||
|
|
||||||
|
function getCornerstoneStack(viewportData) {
|
||||||
|
const {
|
||||||
|
displaySetInstanceUid,
|
||||||
|
studyInstanceUid,
|
||||||
|
} = viewportData;
|
||||||
|
|
||||||
|
// Create shortcut to displaySet
|
||||||
|
const study = OHIF.viewer.Studies.findBy({
|
||||||
|
studyInstanceUid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const displaySet = study.displaySets.find((set) => {
|
||||||
|
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get stack from Stack Manager
|
||||||
|
const stack = StackManager.findOrCreateStack(study, displaySet);
|
||||||
|
stack.currentImageIdIndex = 0;
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
class GridLayout extends Component {
|
class GridLayout extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -76,7 +100,7 @@ class GridLayout extends Component {
|
|||||||
|
|
||||||
const viewports = viewportData.map((data, index) => {
|
const viewports = viewportData.map((data, index) => {
|
||||||
const className = `viewportContainer ${this.getClass(index)} ${this.getActiveClass(index)}`;
|
const className = `viewportContainer ${this.getClass(index)} ${this.getActiveClass(index)}`;
|
||||||
|
|
||||||
// TODO[react]: Not sure why I needed to do this. Looks like
|
// TODO[react]: Not sure why I needed to do this. Looks like
|
||||||
// empty viewports aren't provided with a viewportIndex normally?
|
// empty viewports aren't provided with a viewportIndex normally?
|
||||||
data.viewportIndex = index;
|
data.viewportIndex = index;
|
||||||
@ -86,16 +110,26 @@ class GridLayout extends Component {
|
|||||||
width: `${width}%`,
|
width: `${width}%`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cornerstoneViewport = (data) => (
|
const cornerstoneViewport = (data) => {
|
||||||
<CornerstoneViewport viewportData={data}/>
|
const stack = getCornerstoneStack(data)
|
||||||
);
|
const viewportData = {
|
||||||
|
stack,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<CornerstoneViewport
|
||||||
|
viewportData={viewportData}
|
||||||
|
cornerstone={cornerstone}
|
||||||
|
cornerstoneTools={cornerstoneTools}
|
||||||
|
/>);
|
||||||
|
};
|
||||||
|
|
||||||
const pluginViewport = (data) => (
|
const pluginViewport = (data) => (
|
||||||
<div className={`viewport-plugin-${data.plugin}`} style="height:100%; width: 100%">
|
<div className={`viewport-plugin-${data.plugin}`} style="height:100%; width: 100%">
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let contents;
|
let contents;
|
||||||
if (!data.studyInstanceUid || !data.displaySetInstanceUid) {
|
if (!data.studyInstanceUid || !data.displaySetInstanceUid) {
|
||||||
contents = (
|
contents = (
|
||||||
<div className="CornerstoneViewport">
|
<div className="CornerstoneViewport">
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ImageScrollbar.styl';
|
|
||||||
|
|
||||||
class ImageScrollbar extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.style = {
|
|
||||||
width: `${this.props.height}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="scroll">
|
|
||||||
<div className="scroll-holder">
|
|
||||||
<input
|
|
||||||
className="imageSlider"
|
|
||||||
style={this.style}
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max={this.props.max}
|
|
||||||
step="1"
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onKeyDown={this.onKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(event) {
|
|
||||||
const intValue = parseInt(event.target.value, 10);
|
|
||||||
this.props.onInputCallback(intValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(event) {
|
|
||||||
// We don't allow direct keyboard up/down input on the
|
|
||||||
// image sliders since the natural direction is reversed (0 is at the top)
|
|
||||||
|
|
||||||
// Store the KeyCodes in an object for readability
|
|
||||||
const keys = {
|
|
||||||
DOWN: 40,
|
|
||||||
UP: 38
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.which === keys.DOWN) {
|
|
||||||
OHIF.commands.run('scrollDown');
|
|
||||||
event.preventDefault();
|
|
||||||
} else if (event.which === keys.UP) {
|
|
||||||
OHIF.commands.run('scrollUp');
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageScrollbar.propTypes = {
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
onInputCallback: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImageScrollbar;
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
@import "{ohif:design}/app"
|
|
||||||
|
|
||||||
$imageSliderBorderRadius = 57px
|
|
||||||
$imageSliderTrackColor = rgba(0,0,0,0)
|
|
||||||
$imageSliderBorder = none
|
|
||||||
|
|
||||||
// Note that these are backwards due to the magic needed to make a cross-browser vertical slider
|
|
||||||
$imageSliderWidth = 12px
|
|
||||||
$imageSliderHeight = 39px
|
|
||||||
|
|
||||||
.scroll
|
|
||||||
height: 100%
|
|
||||||
padding: 5px
|
|
||||||
position: absolute
|
|
||||||
right: 0
|
|
||||||
top: 0
|
|
||||||
|
|
||||||
.scroll-holder
|
|
||||||
height: calc(100% - 20px)
|
|
||||||
margin-top: 5px
|
|
||||||
position: relative
|
|
||||||
width: 12px
|
|
||||||
|
|
||||||
.imageSlider
|
|
||||||
height: $imageSliderWidth
|
|
||||||
left: 12px
|
|
||||||
padding: 0
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
|
|
||||||
vendorize(transform, rotate(90deg))
|
|
||||||
vendorize(transform-origin, top left)
|
|
||||||
|
|
||||||
-webkit-appearance: none
|
|
||||||
background-color: $imageSliderTrackColor
|
|
||||||
|
|
||||||
// Remove focus highlights on range input
|
|
||||||
&:focus
|
|
||||||
outline: none
|
|
||||||
vendorize(box-shadow, none)
|
|
||||||
|
|
||||||
// Remove focus border in Firefox
|
|
||||||
&::-moz-focus-outer
|
|
||||||
border: none
|
|
||||||
|
|
||||||
// --- Style the range track --- //
|
|
||||||
&::-webkit-slider-runnable-track
|
|
||||||
background-color: $imageSliderTrackColor
|
|
||||||
border: none
|
|
||||||
cursor: pointer
|
|
||||||
height: 5px
|
|
||||||
z-index: 6
|
|
||||||
|
|
||||||
&::-moz-range-track
|
|
||||||
background-color: $imageSliderTrackColor
|
|
||||||
border: none
|
|
||||||
cursor: pointer
|
|
||||||
height: 2px
|
|
||||||
z-index: 6
|
|
||||||
|
|
||||||
&::-ms-track
|
|
||||||
animate: 0.2s
|
|
||||||
background: transparent
|
|
||||||
border: none
|
|
||||||
border-width: 15px 0
|
|
||||||
color: $imageSliderTrackColor
|
|
||||||
cursor: pointer
|
|
||||||
height: $imageSliderWidth
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
// Hide any fill IE tries to add
|
|
||||||
&::-ms-fill-lower
|
|
||||||
background: $imageSliderTrackColor
|
|
||||||
|
|
||||||
&::-ms-fill-upper
|
|
||||||
background: $imageSliderTrackColor
|
|
||||||
|
|
||||||
// --- Style the range thumb --- //
|
|
||||||
&::-webkit-slider-thumb
|
|
||||||
-webkit-appearance: none !important
|
|
||||||
theme('background-color', '$imageSliderColor')
|
|
||||||
border: $imageSliderBorder
|
|
||||||
border-radius: $imageSliderBorderRadius
|
|
||||||
cursor: -webkit-grab
|
|
||||||
height: $imageSliderWidth
|
|
||||||
margin-top: -4px
|
|
||||||
width: $imageSliderHeight
|
|
||||||
|
|
||||||
&:active
|
|
||||||
theme('background-color', '$activeColor')
|
|
||||||
cursor: -webkit-grabbing
|
|
||||||
|
|
||||||
&::-moz-range-thumb
|
|
||||||
theme('background-color', '$imageSliderColor')
|
|
||||||
border: $imageSliderBorder
|
|
||||||
border-radius: $imageSliderBorderRadius
|
|
||||||
cursor: -moz-grab
|
|
||||||
height: $imageSliderWidth
|
|
||||||
width: $imageSliderHeight
|
|
||||||
z-index: 7
|
|
||||||
|
|
||||||
&:active
|
|
||||||
theme('background-color', '$activeColor')
|
|
||||||
cursor: -moz-grabbing
|
|
||||||
|
|
||||||
&::-ms-thumb
|
|
||||||
theme('background-color', '$imageSliderColor')
|
|
||||||
border: $imageSliderBorder
|
|
||||||
border-radius: $imageSliderBorderRadius
|
|
||||||
cursor: ns-resize
|
|
||||||
height: $imageSliderWidth
|
|
||||||
width: $imageSliderHeight
|
|
||||||
|
|
||||||
&:active
|
|
||||||
theme('background-color', '$activeColor')
|
|
||||||
|
|
||||||
&::-ms-tooltip
|
|
||||||
display: none
|
|
||||||
|
|
||||||
// Set left position in IE, border-width attribute breaks left position of imageSlider
|
|
||||||
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none)
|
|
||||||
.imageSlider
|
|
||||||
left: 50px
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { $ } from 'meteor/jquery';
|
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
|
|
||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './LoadingIndicator.styl';
|
|
||||||
|
|
||||||
Meteor.startup(() => {
|
|
||||||
// This checking is necessary because cornerstoneTools may not have some tools available.
|
|
||||||
// Example: when an app defines its own cornerstone's lib versions, so it
|
|
||||||
// uses only ohif-viewerbase and not ohif-cornerstone and those libs are added later.
|
|
||||||
if (cornerstoneTools.loadHandlerManager) {
|
|
||||||
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
|
|
||||||
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
|
|
||||||
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let loadHandlerTimeout;
|
|
||||||
|
|
||||||
const startLoadingHandler = element => {
|
|
||||||
clearTimeout(loadHandlerTimeout);
|
|
||||||
loadHandlerTimeout = setTimeout(() => {
|
|
||||||
console.log('startLoading');
|
|
||||||
const elem = $(element);
|
|
||||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
|
||||||
elem.find('canvas').not('.magnifyTool').addClass('faded');
|
|
||||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
|
||||||
}, OHIF.viewer.loadIndicatorDelay);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doneLoadingHandler = element => {
|
|
||||||
clearTimeout(loadHandlerTimeout);
|
|
||||||
const elem = $(element);
|
|
||||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
|
||||||
elem.find('canvas').not('.magnifyTool').removeClass('faded');
|
|
||||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorLoadingHandler = (element, imageId, error, source) => {
|
|
||||||
clearTimeout(loadHandlerTimeout);
|
|
||||||
const elem = $(element);
|
|
||||||
|
|
||||||
// Could probably chain all of these, but this is more readable
|
|
||||||
elem.find('canvas').not('.magnifyTool').removeClass('faded');
|
|
||||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
|
||||||
|
|
||||||
// Don't display errors from the stackPrefetch tool
|
|
||||||
if (source === 'stackPrefetch') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
|
|
||||||
errorLoadingIndicator.css('display', 'block');
|
|
||||||
|
|
||||||
// This is just used to expand upon some error messages that are sent
|
|
||||||
// when things fail. An example is a network error throwing the error
|
|
||||||
// which is only described as "network".
|
|
||||||
const errorDetails = {
|
|
||||||
network: 'A network error has occurred'
|
|
||||||
// We need to expand this further when we see more obscure error messages
|
|
||||||
};
|
|
||||||
|
|
||||||
if (errorDetails.hasOwnProperty(error)) {
|
|
||||||
error = errorDetails[error];
|
|
||||||
}
|
|
||||||
|
|
||||||
errorLoadingIndicator.find('.description').text(`An error has occurred while loading image: ${imageId}`);
|
|
||||||
if (error) {
|
|
||||||
errorLoadingIndicator.find('.details').text(`Details: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class LoadingIndicator extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
// TODO[react]: Pass this in as a prop reactively
|
|
||||||
//const percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
|
|
||||||
let percComplete;
|
|
||||||
if (this.props.percentComplete && this.props.percentComplete !== 100) {
|
|
||||||
percComplete = `${this.props.percentComplete}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
|
||||||
<div className="indicatorContents">
|
|
||||||
<p>Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw"></i> {percComplete}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="imageViewerErrorLoadingIndicator loadingIndicator">
|
|
||||||
<div className="indicatorContents">
|
|
||||||
<h4>Error Loading Image</h4>
|
|
||||||
<p className='description'>An error has occurred.</p>
|
|
||||||
<p className='details'></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingIndicator.propTypes = {
|
|
||||||
percentComplete: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoadingIndicator;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
@import "{ohif:design}/app"
|
|
||||||
|
|
||||||
.imageViewerLoadingIndicator
|
|
||||||
theme('color', '$textSecondaryColor')
|
|
||||||
|
|
||||||
.faded
|
|
||||||
opacity: 0.5
|
|
||||||
|
|
||||||
.imageViewerErrorLoadingIndicator
|
|
||||||
theme('color', '$uiYellow')
|
|
||||||
|
|
||||||
p, h4
|
|
||||||
padding: 4px 0
|
|
||||||
text-align: center
|
|
||||||
word-wrap: break-word
|
|
||||||
|
|
||||||
p
|
|
||||||
font-size: 11pt
|
|
||||||
|
|
||||||
.loadingIndicator
|
|
||||||
background-color: rgba(0,0,0,0.75)
|
|
||||||
display: none
|
|
||||||
font-size: 18px
|
|
||||||
height: 100%
|
|
||||||
overflow: hidden
|
|
||||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
width: 100%
|
|
||||||
z-index: 1
|
|
||||||
|
|
||||||
.indicatorContents
|
|
||||||
font-size: 30px
|
|
||||||
font-weight: 300
|
|
||||||
position: absolute
|
|
||||||
text-align: center
|
|
||||||
top: 50%
|
|
||||||
transform(translateY(-50%))
|
|
||||||
width: 100%
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ViewportOrientationMarkers.styl';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the orientation labels on a Cornerstone-enabled Viewport element
|
|
||||||
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
|
|
||||||
*
|
|
||||||
* @param imageId The Cornerstone ImageId
|
|
||||||
* @param viewport The current viewport
|
|
||||||
*/
|
|
||||||
export function getOrientationMarkers(imageId, viewport) {
|
|
||||||
const imagePlane = cornerstone.metaData.get('imagePlane', imageId);
|
|
||||||
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
|
||||||
const columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
|
||||||
const oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
|
||||||
const oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
|
||||||
|
|
||||||
const markers = {
|
|
||||||
top: oppositeColumnString,
|
|
||||||
left: oppositeRowString
|
|
||||||
};
|
|
||||||
|
|
||||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
|
||||||
// the rotation applications
|
|
||||||
if (viewport.vflip) {
|
|
||||||
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewport.hflip) {
|
|
||||||
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap the labels accordingly if the viewport has been rotated
|
|
||||||
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
|
||||||
if (viewport.rotation === 90 || viewport.rotation === -270) {
|
|
||||||
return {
|
|
||||||
top: markers.left,
|
|
||||||
left: cornerstoneTools.orientation.invertOrientationString(markers.top)
|
|
||||||
};
|
|
||||||
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
|
|
||||||
return {
|
|
||||||
top: cornerstoneTools.orientation.invertOrientationString(markers.left),
|
|
||||||
left: markers.top
|
|
||||||
};
|
|
||||||
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
|
|
||||||
return {
|
|
||||||
top: cornerstoneTools.orientation.invertOrientationString(markers.top),
|
|
||||||
left: cornerstoneTools.orientation.invertOrientationString(markers.left)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return markers;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewportOrientationMarkers extends Component {
|
|
||||||
render() {
|
|
||||||
const { imageId, viewport } = this.props;
|
|
||||||
const markers = getOrientationMarkers(imageId, viewport);
|
|
||||||
if (!markers) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ViewportOrientationMarkers noselect">
|
|
||||||
<div className="top-mid orientation-marker">
|
|
||||||
{markers.top}
|
|
||||||
</div>
|
|
||||||
<div className="left-mid orientation-marker">
|
|
||||||
{markers.left}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewportOrientationMarkers.propTypes = {
|
|
||||||
imageId: PropTypes.string.isRequired,
|
|
||||||
viewport: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewportOrientationMarkers;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.ViewportOrientationMarkers
|
|
||||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
|
||||||
|
|
||||||
font-size: 15px
|
|
||||||
color: rgb(204, 204, 204)
|
|
||||||
line-height: 18px
|
|
||||||
|
|
||||||
.orientation-marker
|
|
||||||
position: absolute
|
|
||||||
|
|
||||||
.top-mid
|
|
||||||
top: 5px
|
|
||||||
left: 50%
|
|
||||||
|
|
||||||
.left-mid
|
|
||||||
top: 47%
|
|
||||||
left: 5px
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './ViewportOverlay.styl';
|
|
||||||
import { viewportOverlayUtils } from '../../../lib/viewportOverlayUtils';
|
|
||||||
import { helpers } from '../../../lib/helpers/index.js';
|
|
||||||
const { formatPN, formatDA, formatNumberPrecision, formatTM, isValidNumber} = helpers;
|
|
||||||
|
|
||||||
function getCompression(imageId) {
|
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
if (!instance) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.lossyImageCompression === '01' &&
|
|
||||||
instance.lossyImageCompressionRatio !== '') {
|
|
||||||
const compressionMethod = instance.lossyImageCompressionMethod || 'Lossy: ';
|
|
||||||
const compressionRatio = formatNumberPrecision(instance.lossyImageCompressionRatio, 2);
|
|
||||||
return compressionMethod + compressionRatio + ' : 1';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Lossless / Uncompressed';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageDimensions(imageId) {
|
|
||||||
const instance = cornerstone.metaData.get('instance', imageId)
|
|
||||||
if (!instance) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${instance.columns} x ${instance.rows}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewportOverlay extends Component {
|
|
||||||
render() {
|
|
||||||
const zoom = this.props.viewport.scale * 100;
|
|
||||||
const imageId = this.props.imageId;
|
|
||||||
|
|
||||||
const patientMetadata = cornerstone.metaData.get('patient', imageId);
|
|
||||||
const studyMetadata = cornerstone.metaData.get('study', imageId);
|
|
||||||
const seriesMetadata = cornerstone.metaData.get('series', imageId);
|
|
||||||
const instanceMetadata = cornerstone.metaData.get('instance', imageId);
|
|
||||||
|
|
||||||
const patientName = patientMetadata.name;
|
|
||||||
const patientId = patientMetadata.id;;
|
|
||||||
const { studyDate, studyTime, studyDescription } = studyMetadata;
|
|
||||||
const { seriesNumber, seriesDescription } = seriesMetadata;
|
|
||||||
const { instanceNumber, frameTime, thickness, spacingBetweenSlices, sliceLocation } = instanceMetadata;
|
|
||||||
const frameRate = formatNumberPrecision(1000 / frameTime, 1);
|
|
||||||
const compression = getCompression(imageId);
|
|
||||||
const windowWidth = this.props.viewport.voi.windowWidth || 0;
|
|
||||||
const windowCenter = this.props.viewport.voi.windowCenter || 0;
|
|
||||||
const wwwc = `W: ${windowWidth.toFixed(0)} L: ${windowCenter.toFixed(0)}`;
|
|
||||||
const imageIndex = this.props.stack.imageIds.indexOf(this.props.imageId) + 1;
|
|
||||||
const numImages = this.props.stack.imageIds.length;
|
|
||||||
const imageDimensions = getImageDimensions(imageId);
|
|
||||||
|
|
||||||
const normal = (<>
|
|
||||||
<div className="top-left overlay-element">
|
|
||||||
<div>{formatPN(patientName)}</div>
|
|
||||||
<div>{patientId}</div>
|
|
||||||
</div>
|
|
||||||
<div className="top-right overlay-element">
|
|
||||||
<div>{studyDescription}</div>
|
|
||||||
<div>{formatDA(studyDate)} {formatTM(studyTime)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bottom-right overlay-element">
|
|
||||||
<div>Zoom: {formatNumberPrecision(zoom,0)}%</div>
|
|
||||||
<div className="compressionIndicator">{compression}</div>
|
|
||||||
<div>{wwwc}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bottom-left overlay-element">
|
|
||||||
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
|
|
||||||
<div>{numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''}</div>
|
|
||||||
<div>{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}
|
|
||||||
<div>{imageDimensions}</div>
|
|
||||||
<div>
|
|
||||||
<span>{isValidNumber(sliceLocation) ? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm` : ''}</span>
|
|
||||||
<span>{thickness ? `Thick: ${formatNumberPrecision(thickness, 2)} mm` : ''}</span>
|
|
||||||
<span>{spacingBetweenSlices ? `Spacing: ${formatNumberPrecision(spacingBetweenSlices, 2)} mm` : ''}</span>
|
|
||||||
</div>
|
|
||||||
<div>{seriesDescription}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
|
|
||||||
const rightOnly = (<>
|
|
||||||
<div className="top-right overlay-element">
|
|
||||||
<div>{formatPN(patientName)}</div>
|
|
||||||
<div>{patientId}</div>
|
|
||||||
<div>{studyDescription}</div>
|
|
||||||
<div>{formatDA(studyDate)} {formatTM(studyTime)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bottom-right overlay-element">
|
|
||||||
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
|
|
||||||
<div>{numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''}</div>
|
|
||||||
<div>{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}</div>
|
|
||||||
<div>{imageDimensions}</div>
|
|
||||||
<div>{seriesDescription}</div>
|
|
||||||
<div>Zoom: ${formatNumberPrecision(zoom, 0)}%</div>
|
|
||||||
<div className="compressionIndicator">{compression}</div>
|
|
||||||
<div>{wwwc}</div>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
|
|
||||||
const leftOnly = (<>
|
|
||||||
<div className="top-left overlay-element">
|
|
||||||
<div>{formatPN(patientName)}</div>
|
|
||||||
<div>{patientId}</div>
|
|
||||||
<div>{studyDescription}</div>
|
|
||||||
<div>{formatDA(studyDate)} {formatTM(studyTime)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bottom-left overlay-element">
|
|
||||||
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
|
|
||||||
<div>{numImages > 1 ? `Img: ${instanceNumber} ${imageIndex}/${numImages}` : ''}</div>
|
|
||||||
<div>{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}</div>
|
|
||||||
<div>{imageDimensions}</div>
|
|
||||||
<div>{seriesDescription}</div>
|
|
||||||
<div>Zoom: ${formatNumberPrecision(zoom, 0)}%</div>
|
|
||||||
<div className="compressionIndicator">{compression}</div>
|
|
||||||
<div>{wwwc}</div>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='ViewportOverlay'>
|
|
||||||
{normal}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewportOverlay.propTypes = {
|
|
||||||
viewport: PropTypes.object.isRequired,
|
|
||||||
imageId: PropTypes.string.isRequired,
|
|
||||||
stack: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewportOverlay;
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
@import "{ohif:design}/app"
|
|
||||||
|
|
||||||
$viewportTagPadding = 20px
|
|
||||||
|
|
||||||
.imageViewerViewport.empty ~ .ViewportOverlay
|
|
||||||
display: none
|
|
||||||
|
|
||||||
.ViewportOverlay
|
|
||||||
theme('color', '$textSecondaryColor')
|
|
||||||
|
|
||||||
.overlay-element
|
|
||||||
position: absolute
|
|
||||||
font-weight: 400
|
|
||||||
text-shadow: 1px 1px black
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
.top-left
|
|
||||||
top: $viewportTagPadding
|
|
||||||
left: $viewportTagPadding
|
|
||||||
|
|
||||||
.top-center
|
|
||||||
top: $viewportTagPadding
|
|
||||||
padding-top: $viewportTagPadding
|
|
||||||
width: 100%
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.top-right
|
|
||||||
top: $viewportTagPadding
|
|
||||||
right: $viewportTagPadding
|
|
||||||
text-align: right
|
|
||||||
|
|
||||||
.bottom-left
|
|
||||||
bottom: $viewportTagPadding
|
|
||||||
left: $viewportTagPadding
|
|
||||||
|
|
||||||
.bottom-right
|
|
||||||
bottom: $viewportTagPadding
|
|
||||||
right: $viewportTagPadding
|
|
||||||
text-align: right
|
|
||||||
|
|
||||||
&.controlsVisible
|
|
||||||
.topright, .bottomright
|
|
||||||
right: "calc(%s + 19px)" % $viewportTagPadding
|
|
||||||
|
|
||||||
svg
|
|
||||||
theme('color', '$defaultColor')
|
|
||||||
theme('fill', '$defaultColor')
|
|
||||||
theme('stroke', '$defaultColor')
|
|
||||||
background-color: transparent
|
|
||||||
margin: 2px
|
|
||||||
width: 18px
|
|
||||||
height: 18px
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
import { Template } from 'meteor/templating';
|
|
||||||
import { Session } from 'meteor/session';
|
|
||||||
import { _ } from 'meteor/underscore';
|
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
|
||||||
import { viewportOverlayUtils } from '../../../lib/viewportOverlayUtils';
|
|
||||||
import { getElementIfNotEmpty } from '../../../lib/getElementIfNotEmpty';
|
|
||||||
import { getStackDataIfNotEmpty } from '../../../lib/getStackDataIfNotEmpty';
|
|
||||||
|
|
||||||
Template.viewportOverlay.onCreated(() => {
|
|
||||||
const instance = Template.instance();
|
|
||||||
|
|
||||||
instance.getImageIndex = () => {
|
|
||||||
const stack = getStackDataIfNotEmpty(instance.data.viewportIndex);
|
|
||||||
if (!stack || stack.currentImageIdIndex === undefined) return;
|
|
||||||
|
|
||||||
return stack.currentImageIdIndex;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.viewportOverlay.helpers({
|
|
||||||
wwwc() {
|
|
||||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
|
||||||
|
|
||||||
const element = getElementIfNotEmpty(this.viewportIndex);
|
|
||||||
if (!element) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstone.getViewport(element);
|
|
||||||
if (!viewport) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
|
||||||
},
|
|
||||||
|
|
||||||
zoom() {
|
|
||||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
|
||||||
|
|
||||||
const element = getElementIfNotEmpty(this.viewportIndex);
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = cornerstone.getViewport(element);
|
|
||||||
if (!viewport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (viewport.scale * 100.0);
|
|
||||||
},
|
|
||||||
|
|
||||||
imageDimensions() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
const image = viewportOverlayUtils.getImage(this.viewportIndex);
|
|
||||||
if (!image) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return image.width + ' x ' + image.height;
|
|
||||||
},
|
|
||||||
|
|
||||||
patientName() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getPatient.call(this, 'name');
|
|
||||||
},
|
|
||||||
|
|
||||||
patientId() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getPatient.call(this, 'id');
|
|
||||||
},
|
|
||||||
|
|
||||||
patientBirthDate() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getPatient.call(this, 'birthDate');
|
|
||||||
},
|
|
||||||
|
|
||||||
patientSex() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getPatient.call(this, 'sex');
|
|
||||||
},
|
|
||||||
|
|
||||||
studyDate() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getStudy.call(this, 'studyDate');
|
|
||||||
},
|
|
||||||
|
|
||||||
studyTime() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getStudy.call(this, 'studyTime');
|
|
||||||
},
|
|
||||||
|
|
||||||
studyDescription() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getStudy.call(this, 'studyDescription');
|
|
||||||
},
|
|
||||||
|
|
||||||
seriesDescription() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getSeries.call(this, 'seriesDescription');
|
|
||||||
},
|
|
||||||
|
|
||||||
frameRate() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
const frameTime = viewportOverlayUtils.getInstance.call(this, 'frameTime');
|
|
||||||
if (!frameTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frameRate = 1000 / frameTime;
|
|
||||||
return frameRate.toFixed(1);
|
|
||||||
},
|
|
||||||
|
|
||||||
seriesNumber() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getSeries.call(this, 'seriesNumber');
|
|
||||||
},
|
|
||||||
|
|
||||||
instanceNumber() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getInstance.call(this, 'instanceNumber');
|
|
||||||
},
|
|
||||||
|
|
||||||
thickness() {
|
|
||||||
// Displays Slice Thickness (0018,0050)
|
|
||||||
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getInstance.call(this, 'sliceThickness');
|
|
||||||
},
|
|
||||||
|
|
||||||
location() {
|
|
||||||
// Displays Slice Location (0020,1041), if present.
|
|
||||||
// - Otherwise, displays Table Position (0018,9327)
|
|
||||||
// - TODO: Otherwise, displays a value derived from Image Position (Patient) (0020,0032)
|
|
||||||
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
const sliceLocation = viewportOverlayUtils.getInstance.call(this, 'sliceLocation');
|
|
||||||
if (sliceLocation !== '') {
|
|
||||||
return sliceLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tablePosition = viewportOverlayUtils.getInstance.call(this, 'tablePosition');
|
|
||||||
if (tablePosition !== '') {
|
|
||||||
return tablePosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getInstance.call(this, 'imagePositionPatient');
|
|
||||||
},
|
|
||||||
|
|
||||||
spacingBetweenSlices() {
|
|
||||||
// Displays Spacing Between Slices (0018,0088), if present.
|
|
||||||
|
|
||||||
// TODO: Otherwise, displays a value derived from successive values
|
|
||||||
// of Image Position (Patient) (0020,0032) perpendicular to
|
|
||||||
// the Image Orientation (Patient) (0020,0037)
|
|
||||||
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getInstance.call(this, 'spacingBetweenSlices');
|
|
||||||
},
|
|
||||||
|
|
||||||
compression() {
|
|
||||||
// Displays whether or not lossy compression has been applied:
|
|
||||||
//
|
|
||||||
// - Checks Lossy Image Compression (0028,2110)
|
|
||||||
// - If so, displays the value of Lossy Image Compression Ratio (0028,2112)
|
|
||||||
// and Lossy Image Compression Method (0028,2114)
|
|
||||||
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
if (!this.imageId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = cornerstone.metaData.get('instance', this.imageId);
|
|
||||||
if (!instance) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.lossyImageCompression === '01' &&
|
|
||||||
instance.lossyImageCompressionRatio !== '') {
|
|
||||||
const compressionMethod = instance.lossyImageCompressionMethod || 'Lossy: ';
|
|
||||||
const compressionRatio = parseFloat(instance.lossyImageCompressionRatio).toFixed(2);
|
|
||||||
return compressionMethod + compressionRatio + ' : 1';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Lossless / Uncompressed';
|
|
||||||
},
|
|
||||||
|
|
||||||
tagDisplayLeftOnly() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getTagDisplay.call(this, 'side') === 'L';
|
|
||||||
},
|
|
||||||
|
|
||||||
tagDisplayRightOnly() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getTagDisplay.call(this, 'side') === 'R';
|
|
||||||
},
|
|
||||||
|
|
||||||
tagDisplaySpecified() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getTagDisplay.call(this, 'side');
|
|
||||||
},
|
|
||||||
|
|
||||||
imageNumber() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
return viewportOverlayUtils.getInstance.call(this, 'number');
|
|
||||||
},
|
|
||||||
|
|
||||||
imageIndex() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
const imageIndex = Template.instance().getImageIndex();
|
|
||||||
return _.isUndefined(imageIndex) ? 0 : imageIndex + 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
numImages() {
|
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
|
||||||
|
|
||||||
const stack = getStackDataIfNotEmpty(this.viewportIndex);
|
|
||||||
if (!stack || !stack.imageIds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stack.imageIds.length;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
<template name="viewportOverlay">
|
|
||||||
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
|
|
||||||
{{#unless tagDisplaySpecified}}
|
|
||||||
<div class="topleft dicomTag">
|
|
||||||
<div>{{formatPN patientName}}</div>
|
|
||||||
<div>{{patientId}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="topright dicomTag">
|
|
||||||
<div>{{studyDescription}}</div>
|
|
||||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottomright dicomTag">
|
|
||||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
|
||||||
<div class="compressionIndicator">{{compression}}</div>
|
|
||||||
<div>{{wwwc}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottomleft dicomTag">
|
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
|
||||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
|
||||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
|
||||||
<div>{{imageDimensions}}</div>
|
|
||||||
<div>
|
|
||||||
<span>{{#if isValidNumber location}}Loc: {{formatNumberPrecision location 2}} mm{{/if}}</span>
|
|
||||||
<span>{{#if thickness}}Thick: {{formatNumberPrecision thickness 2}} mm{{/if}}</span>
|
|
||||||
<span>{{#if spacingBetweenSlices}}Spacing: {{formatNumberPrecision spacingBetweenSlices 2}} mm{{/if}}</span>
|
|
||||||
</div>
|
|
||||||
<div>{{seriesDescription}}</div>
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
{{#if tagDisplayRightOnly}}
|
|
||||||
<div class="topright dicomTag">
|
|
||||||
<div>{{formatPN patientName}}</div>
|
|
||||||
<div>{{patientId}}</div>
|
|
||||||
<div>{{studyDescription}}</div>
|
|
||||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottomright dicomTag">
|
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
|
||||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
|
||||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
|
||||||
<div>{{imageDimensions}}</div>
|
|
||||||
<div>{{seriesDescription}}</div>
|
|
||||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
|
||||||
<div class="compressionIndicator">{{compression}}</div>
|
|
||||||
<div>{{wwwc}}</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if tagDisplayLeftOnly}}
|
|
||||||
<div class="topleft dicomTag">
|
|
||||||
<div>{{formatPN patientName}}</div>
|
|
||||||
<div>{{patientId}}</div>
|
|
||||||
<div>{{studyDescription}}</div>
|
|
||||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottomleft dicomTag">
|
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
|
||||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
|
||||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
|
||||||
<div>{{imageDimensions}}</div>
|
|
||||||
<div>{{seriesDescription}}</div>
|
|
||||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
|
||||||
<div class="compressionIndicator">{{compression}}</div>
|
|
||||||
<div>{{wwwc}}</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if gt numImages 1}}
|
|
||||||
{{>imageControls imageIndex=imageIndex numImages=numImages}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,20 +1,11 @@
|
|||||||
import ViewerMain from './ViewerMain/ViewerMain.js';
|
import ViewerMain from './ViewerMain/ViewerMain.js';
|
||||||
import GridLayout from './GridLayout/GridLayout.js';
|
import GridLayout from './GridLayout/GridLayout.js';
|
||||||
import LoadingIndicator from './LoadingIndicator/LoadingIndicator.js';
|
|
||||||
import CornerstoneViewport from './CornerstoneViewport/CornerstoneViewport.js';
|
|
||||||
import ImageScrollbar from './ImageScrollbar/ImageScrollbar.js';
|
|
||||||
import ToolContextMenu from './ToolContextMenu/ToolContextMenu.js';
|
import ToolContextMenu from './ToolContextMenu/ToolContextMenu.js';
|
||||||
import ViewportOverlay from './ViewportOverlay/ViewportOverlay.js';
|
|
||||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers/ViewportOrientationMarkers.js';
|
|
||||||
|
|
||||||
const viewer = {
|
const viewer = {
|
||||||
ViewerMain,
|
ViewerMain,
|
||||||
GridLayout,
|
GridLayout,
|
||||||
LoadingIndicator,
|
|
||||||
CornerstoneViewport,
|
|
||||||
ImageScrollbar,
|
|
||||||
ToolContextMenu,
|
ToolContextMenu,
|
||||||
ViewportOverlay,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default viewer;
|
export default viewer;
|
||||||
|
|||||||
@ -146,3 +146,4 @@ const StackManager = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { StackManager };
|
export { StackManager };
|
||||||
|
export default StackManager;
|
||||||
|
|||||||
@ -4,6 +4,16 @@ Package.describe({
|
|||||||
version: '0.0.1'
|
version: '0.0.1'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Npm.depends({
|
||||||
|
'react-cornerstone-viewport': 'git://github.com:cornerstonejs/react-cornerstone-viewport.git',
|
||||||
|
hammerjs: '2.0.8',
|
||||||
|
'cornerstone-core': '2.2.8',
|
||||||
|
'cornerstone-tools': '3.0.0-b.1471',
|
||||||
|
'cornerstone-math': '0.1.7',
|
||||||
|
'dicom-parser': '1.8.3',
|
||||||
|
'cornerstone-wado-image-loader': '2.2.3',
|
||||||
|
});
|
||||||
|
|
||||||
Package.onUse(function(api) {
|
Package.onUse(function(api) {
|
||||||
api.versionsFrom('1.7');
|
api.versionsFrom('1.7');
|
||||||
|
|
||||||
|
|||||||
442
package-lock.json
generated
442
package-lock.json
generated
@ -1,445 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "ohif-viewers-meta",
|
"name": "ohif-viewers-meta",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1
|
||||||
"requires": true,
|
|
||||||
"dependencies": {
|
|
||||||
"bcrypt": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-DwB7WgJPdskbR+9Y3OTJtwRq09Lmm7Na6b+4ewvXjkD0nfNRi1OozxljHm5ETlDCBq9DTy04lQz+rj+T2ztIJg==",
|
|
||||||
"requires": {
|
|
||||||
"nan": "2.10.0",
|
|
||||||
"node-pre-gyp": "0.9.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"ansi-regex": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"aproba": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"are-we-there-yet": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"delegates": "^1.0.0",
|
|
||||||
"readable-stream": "^2.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"balanced-match": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chownr": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"code-point-at": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"console-control-strings": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"core-util-is": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"version": "2.6.9",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deep-extend": {
|
|
||||||
"version": "0.4.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"delegates": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"detect-libc": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"fs-minipass": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"minipass": "^2.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fs.realpath": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"gauge": {
|
|
||||||
"version": "2.7.4",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"aproba": "^1.0.3",
|
|
||||||
"console-control-strings": "^1.0.0",
|
|
||||||
"has-unicode": "^2.0.0",
|
|
||||||
"object-assign": "^4.1.0",
|
|
||||||
"signal-exit": "^3.0.0",
|
|
||||||
"string-width": "^1.0.1",
|
|
||||||
"strip-ansi": "^3.0.1",
|
|
||||||
"wide-align": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glob": {
|
|
||||||
"version": "7.1.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.0.4",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"has-unicode": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"iconv-lite": {
|
|
||||||
"version": "0.4.21",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"safer-buffer": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ignore-walk": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"minimatch": "^3.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inflight": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inherits": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"ini": {
|
|
||||||
"version": "1.3.5",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"is-fullwidth-code-point": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"number-is-nan": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isarray": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"minimatch": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimist": {
|
|
||||||
"version": "0.0.8",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"minipass": {
|
|
||||||
"version": "2.2.4",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "^5.1.1",
|
|
||||||
"yallist": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"bundled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minizlib": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"minipass": "^2.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mkdirp": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"minimist": "0.0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"needle": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"debug": "^2.1.2",
|
|
||||||
"iconv-lite": "^0.4.4",
|
|
||||||
"sax": "^1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-pre-gyp": {
|
|
||||||
"version": "0.9.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"detect-libc": "^1.0.2",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"needle": "^2.2.0",
|
|
||||||
"nopt": "^4.0.1",
|
|
||||||
"npm-packlist": "^1.1.6",
|
|
||||||
"npmlog": "^4.0.2",
|
|
||||||
"rc": "^1.1.7",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"semver": "^5.3.0",
|
|
||||||
"tar": "^4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nopt": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"abbrev": "1",
|
|
||||||
"osenv": "^0.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"npm-bundled": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"npm-packlist": {
|
|
||||||
"version": "1.1.10",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"ignore-walk": "^3.0.1",
|
|
||||||
"npm-bundled": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"npmlog": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"are-we-there-yet": "~1.1.2",
|
|
||||||
"console-control-strings": "~1.1.0",
|
|
||||||
"gauge": "~2.7.3",
|
|
||||||
"set-blocking": "~2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number-is-nan": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"once": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"os-homedir": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"os-tmpdir": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"osenv": {
|
|
||||||
"version": "0.1.5",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"os-homedir": "^1.0.0",
|
|
||||||
"os-tmpdir": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path-is-absolute": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"process-nextick-args": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"rc": {
|
|
||||||
"version": "1.2.6",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"deep-extend": "~0.4.0",
|
|
||||||
"ini": "~1.3.0",
|
|
||||||
"minimist": "^1.2.0",
|
|
||||||
"strip-json-comments": "~2.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"minimist": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"bundled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "2.3.5",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.3",
|
|
||||||
"isarray": "~1.0.0",
|
|
||||||
"process-nextick-args": "~2.0.0",
|
|
||||||
"safe-buffer": "~5.1.1",
|
|
||||||
"string_decoder": "~1.0.3",
|
|
||||||
"util-deprecate": "~1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rimraf": {
|
|
||||||
"version": "2.6.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"glob": "^7.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"sax": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"semver": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"set-blocking": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"signal-exit": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"string-width": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"code-point-at": "^1.0.0",
|
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
|
||||||
"strip-ansi": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "~5.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-ansi": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"ansi-regex": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-json-comments": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"tar": {
|
|
||||||
"version": "4.4.1",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"chownr": "^1.0.1",
|
|
||||||
"fs-minipass": "^1.2.5",
|
|
||||||
"minipass": "^2.2.4",
|
|
||||||
"minizlib": "^1.1.0",
|
|
||||||
"mkdirp": "^0.5.0",
|
|
||||||
"safe-buffer": "^5.1.1",
|
|
||||||
"yallist": "^3.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"bundled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"util-deprecate": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true
|
|
||||||
},
|
|
||||||
"wide-align": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"bundled": true,
|
|
||||||
"requires": {
|
|
||||||
"string-width": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wrappy": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"bundled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitbook-plugin-edit-link": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/gitbook-plugin-edit-link/-/gitbook-plugin-edit-link-2.0.2.tgz",
|
|
||||||
"integrity": "sha1-2PzZJ+ztgeemYqctWdtgnq/X5y8="
|
|
||||||
},
|
|
||||||
"gitbook-plugin-github": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/gitbook-plugin-github/-/gitbook-plugin-github-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-Z0V9+YpX6o75slGLiDQNs3ClMXs="
|
|
||||||
},
|
|
||||||
"nan": {
|
|
||||||
"version": "2.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
|
||||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user