refactor(viewport): Switch to react-cornerstone-viewport

This commit is contained in:
Erik Ziegler 2018-12-06 13:10:46 +01:00
parent a42cc5f1a1
commit 943efb44e5
20 changed files with 57 additions and 2917 deletions

View File

@ -6,11 +6,11 @@ Package.describe({
Npm.depends({
hammerjs: '2.0.8',
'cornerstone-core': '2.2.7',
'cornerstone-tools': '3.0.0-b.1421',
'cornerstone-math': '0.1.6',
'dicom-parser': '1.8.0',
'cornerstone-wado-image-loader': '2.1.4',
'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',
'dcmjs': '0.2.1'
});

View File

@ -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;

View File

@ -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

View File

@ -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>

View File

@ -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);
}
});

View File

@ -2,13 +2,37 @@ import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { OHIF } from 'meteor/ohif:core';
import CornerstoneViewport from '../CornerstoneViewport/CornerstoneViewport.js';
import CornerstoneViewport from 'react-cornerstone-viewport';
import './GridLayout.styl';
import StackManager from '../../../lib/StackManager.js';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
const TOP_CLASS = 'top';
const BOTTOM_CLASS = 'bottom';
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 {
constructor(props) {
super(props);
@ -76,7 +100,7 @@ class GridLayout extends Component {
const viewports = viewportData.map((data, index) => {
const className = `viewportContainer ${this.getClass(index)} ${this.getActiveClass(index)}`;
// TODO[react]: Not sure why I needed to do this. Looks like
// empty viewports aren't provided with a viewportIndex normally?
data.viewportIndex = index;
@ -86,16 +110,26 @@ class GridLayout extends Component {
width: `${width}%`,
};
const cornerstoneViewport = (data) => (
<CornerstoneViewport viewportData={data}/>
);
const cornerstoneViewport = (data) => {
const stack = getCornerstoneStack(data)
const viewportData = {
stack,
...data
};
return (<CornerstoneViewport
viewportData={viewportData}
cornerstone={cornerstone}
cornerstoneTools={cornerstoneTools}
/>);
};
const pluginViewport = (data) => (
<div className={`viewport-plugin-${data.plugin}`} style="height:100%; width: 100%">
</div>
);
let contents;
let contents;
if (!data.studyInstanceUid || !data.displaySetInstanceUid) {
contents = (
<div className="CornerstoneViewport">

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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%

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;
},
});

View File

@ -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>

View File

@ -1,20 +1,11 @@
import ViewerMain from './ViewerMain/ViewerMain.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 ViewportOverlay from './ViewportOverlay/ViewportOverlay.js';
import ViewportOrientationMarkers from './ViewportOrientationMarkers/ViewportOrientationMarkers.js';
const viewer = {
ViewerMain,
GridLayout,
LoadingIndicator,
CornerstoneViewport,
ImageScrollbar,
ToolContextMenu,
ViewportOverlay,
};
export default viewer;

View File

@ -146,3 +146,4 @@ const StackManager = {
};
export { StackManager };
export default StackManager;

View File

@ -4,6 +4,16 @@ Package.describe({
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) {
api.versionsFrom('1.7');

442
package-lock.json generated
View File

@ -1,445 +1,5 @@
{
"name": "ohif-viewers-meta",
"version": "0.0.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=="
}
}
"lockfileVersion": 1
}