ohif-viewer/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js

579 lines
24 KiB
JavaScript

import { OHIF } from 'meteor/ohif:core';
var allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsMultiTouchDragStart';
/**
* This function loads a study series into a viewport element.
*
* @param data {object} Object containing the study, series, and viewport element to be used
*/
function loadDisplaySetIntoViewport(data, templateData) {
log.info('imageViewerViewport loadDisplaySetIntoViewport');
// Make sure we have all the data required to render the series
if (!data.study || !data.displaySet || !data.element) {
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
var element = data.element;
var viewportIndex = $('.imageViewerViewport').index(element);
layoutManager = layoutManager || window.layoutManager;
layoutManager.viewportData = layoutManager.viewportData || {};
if (layoutManager.viewportData[viewportIndex] === undefined) {
layoutManager.viewportData[viewportIndex] = {};
}
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
// Get the contentID of the current worklist tab, if the viewport is running
// alongside the worklist package
var contentId = Session.get('activeContentId');
// If the viewer is inside a tab, create an object related to the specified viewport
// This data will be saved so that the tab can be reloaded to the same state after tabs
// are switched
if (contentId) {
if (!ViewerData[contentId]) {
return;
}
ViewerData[contentId].loadedSeriesData[viewportIndex] = {};
}
// Create an empty array to populate with image IDs
var imageIds = [];
// Loop through the current series and add metadata to the
// Cornerstone meta data provider. This will be used to fill information
// into the viewport overlays, and to calculate reference lines and orientation markers
var displaySet = data.displaySet;
var numImages = displaySet.images.length;
var imageId;
displaySet.images.forEach(function(image, imageIndex) {
var metaData = {
instance: image,
series: displaySet, // TODO: Check this
study: data.study,
numImages: numImages,
imageIndex: imageIndex + 1
};
var numFrames = image.numImageFrames;
if (numFrames > 1) {
log.info('Multiframe image detected');
for (var i = 0; i < numFrames; i++) {
metaData.frame = i;
imageId = getImageId(image, i);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
} else {
imageId = getImageId(image);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
});
// Define the current image stack using the newly created image IDs
var stack = {
currentImageIdIndex: data.currentImageIdIndex || 0,
imageIds: imageIds
};
// Show or hide the image scrollbar depending
// on the number of images in the stack
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var imageControls = currentOverlay.find('.imageControls');
currentOverlay.find('.imageControls').height($(element).height());
if (stack.imageIds.length === 1) {
imageControls.hide();
currentOverlay.find('.topright, .bottomright').css('right', '3px');
} else {
imageControls.show();
currentOverlay.find('.topright, .bottomright').css('right', '39px');
// Update the maximum value of the slider
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.attr('max', stack.imageIds.length);
currentImageSlider.val(1);
// Set it's width to its parent's height
// (because webkit is stupid and can't style vertical sliders)
var scrollbar = currentOverlay.find('#scrollbar');
scrollbar.height(scrollbar.parent().height() - 20);
var overlayHeight = currentImageSlider.parent().height();
currentImageSlider.width(overlayHeight);
}
// Get the current image ID for the stack that will be rendered
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.
ViewportLoading[viewportIndex] = imageId;
// Enable Cornerstone for the viewport element
//
var options = {
renderer: 'webgl'
};
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
// If you have problems, replace it with this line instead:
// cornerstone.enable(element);
cornerstone.enable(element, options);
// 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.
var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
// Start loading the image.
cornerstone.loadAndCacheImage(imageId).then(function(image) {
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch (error) {
log.warn('Viewport destroyed before loaded image could be displayed');
return;
}
// 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);
// Check if there are default viewport settings for this sopClassUid
if (!displaySet.images || !displaySet.images.length) {
return;
}
var instance = displaySet.images[0];
var 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);
}
// Remove the data for this viewport from the ViewportLoading object
// This will stop the loading percentage complete from being displayed.
delete ViewportLoading[viewportIndex];
// Call the handler function that represents the end of the image loading phase
// (e.g. hide the progress text box)
endLoadingHandler(element);
// 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']);
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
// 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
var frameRate = 1000 / displaySet.images[0].frameTime;
var cineToolData = {
loop: OHIF.viewer.cine.loop,
framesPerSecond: frameRate || OHIF.viewer.cine.framesPerSecond
};
cornerstoneTools.addToolState(element, 'playClip', cineToolData);
// Autoplay datasets that have framerates set
if (frameRate) {
cornerstoneTools.playClip(element);
}
// Enable mouse, mouseWheel, touch, and keyboard input on the element
cornerstoneTools.mouseInput.enable(element);
cornerstoneTools.touchInput.enable(element);
cornerstoneTools.mouseWheelInput.enable(element);
cornerstoneTools.keyboardInput.enable(element);
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool, [element]);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
function onImageRendered(e, eventData) {
log.info('imageViewerViewport onImageRendered');
// 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, Random.id());
// Save the current viewport into the ViewerData global variable, as well as the
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
var viewport = cornerstone.getViewport(element);
layoutManager.viewportData[viewportIndex].viewport = viewport;
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
Session.set('ViewerData', ViewerData);
}
// Attach the onImageRendered callback to the CornerstoneImageRendered event
$(element).off('CornerstoneImageRendered', onImageRendered);
$(element).on('CornerstoneImageRendered', onImageRendered);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Define a function to run whenever the Cornerstone viewport changes images
// (e.g. during scrolling)
function onNewImage(e, eventData) {
log.info('imageViewerViewport onNewImage');
// 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, Random.id());
layoutManager.viewportData[viewportIndex].imageId = eventData.enabledElement.image.imageId;
// Get the element and stack data
var element = e.target;
var toolData = cornerstoneTools.getToolState(element, 'stack');
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
var stack = toolData.data[0];
// Update the imageSlider value
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.val(stack.currentImageIdIndex + 1);
// If this viewport is displaying a stack of images, save the current image
// index in the stack to the global ViewerData object, as well as the Meteor Session.
var stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
layoutManager.viewportData[viewportIndex].currentImageIdIndex = imageIdIndex;
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
Session.set('ViewerData', ViewerData);
}
}
// Attach the onNewImage callback to the CornerstoneNewImage event
$(element).off('CornerstoneNewImage', onNewImage);
$(element).on('CornerstoneNewImage', onNewImage);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
function OnStackScroll(e, eventData) {
// Get the element and stack data
var element = e.target;
var toolData = cornerstoneTools.getToolState(element, 'stack');
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
var stack = toolData.data[0];
// Update the imageSlider value
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.val(stack.currentImageIdIndex + 1);
}
$(element).off('CornerstoneStackScroll', OnStackScroll);
if (stack.imageIds.length > 1) {
$(element).on('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
function sendActivationTrigger(e, eventData) {
// 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.
var element = eventData.element;
var activeViewportIndex = Session.get('activeViewport');
var viewportIndex = $('.imageViewerViewport').index(element);
// Reset the focus, even if we don't need to re-enable reference lines or prefetching
$(element).focus();
if (viewportIndex === activeViewportIndex) {
return;
}
log.info('imageViewerViewport sendActivationTrigger');
// Otherwise, trigger an 'ActivateViewport' event to be handled by the Template event
// handler
eventData.viewportIndex = viewportIndex;
var customEvent = $.Event('ActivateViewport', eventData);
// Need to overwrite the type set in the original event
customEvent.type = 'ActivateViewport';
$(e.target).trigger(customEvent, eventData);
}
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
// Store the current series data inside the Layout Manager
layoutManager.viewportData[viewportIndex] = {
imageId: imageId,
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport,
viewportIndex: viewportIndex
};
ViewerData[contentId].loadedSeriesData = layoutManager.viewportData;
// Check if image plane (orientation / loction) data is present for the current image
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
// If it is, and reference lines are enabled, add this element to the global synchronizer
// that is used for updating reference lines, and enable reference lines for this viewport.
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
OHIF.viewer.updateImageSynchronizer.add(element);
}
// 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 === Session.get('activeViewport')) {
setActiveViewport(element);
}
// Temporary until we have a real window manager with events for series/study changed
log.info('Set NewSeriesLoaded');
Session.set('NewSeriesLoaded', Random.id());
// Run any renderedCallback that exists in the data context
if (data.renderedCallback && typeof data.renderedCallback === 'function') {
data.renderedCallback(element);
}
}, function(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
*
*/
function setDisplaySet(data, displaySetInstanceUid, templateData) {
var study = data.study;
if (!study || !study.displaySets) {
throw "Study does not exist or has no display sets";
return;
}
study.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) {
throw "Display set not found in specified study!";
return;
}
// Otherwise, load pass the data object into loadSeriesIntoViewport
loadDisplaySetIntoViewport(data, templateData);
}
/**
* This function searches an object to return the keys that contain a specific value
*
* @param object {object} The object to be searched
* @param value The value to be found
*
* @returns {array} The keys for which the object has the specified value
*/
function getKeysByValue(object, value) {
// http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value
return Object.keys(object).filter(function(key) {
return object[key] === value;
});
}
Meteor.startup(function() {
// On Meteor startup, define the global objects used to store loading imageIds
// by viewport / thumbnail element
ViewportLoading = {};
ThumbnailLoading = {};
// Whenever the CornerstoneImageLoadProgress is fired, identify which viewports
// the "in-progress" image is to be displayed in. Then pass the percent complete
// via the Meteor Session to the other templates to be displayed in the relevant viewports.
$(cornerstone).on('CornerstoneImageLoadProgress', function(e, eventData) {
viewportIndices = getKeysByValue(ViewportLoading, eventData.imageId);
viewportIndices.forEach(function(viewportIndex) {
Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete);
});
thumbnailIndices = getKeysByValue(ThumbnailLoading, eventData.imageId);
thumbnailIndices.forEach(function(thumbnailIndex) {
Session.set('CornerstoneThumbnailLoadProgress' + thumbnailIndex, eventData.percentComplete);
});
});
var config = {
magnifySize: 300,
magnificationLevel: 3
};
cornerstoneTools.magnify.setConfiguration(config);
});
Template.imageViewerViewport.onRendered(function() {
var templateData = Template.currentData();
log.info('imageViewerViewport onRendered');
// When the imageViewerViewport template is rendered
var element = this.find('.imageViewerViewport');
// Display the loading indicator for this element
$(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.
var activeViewport = Session.get('activeViewport');
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
var data = {
element: element,
viewport: this.data.viewport,
currentImageIdIndex: this.data.currentImageIdIndex,
displaySetInstanceUid: this.data.displaySetInstanceUid,
studyInstanceUid: this.data.studyInstanceUid,
seriesInstanceUid: this.data.seriesInstanceUid,
renderedCallback: this.data.renderedCallback,
activeViewport: 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 (!this.data.displaySetInstanceUid) {
element.classList.add('empty');
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'none');
$(element).siblings('.viewportInstructions').show();
return;
}
// Look through the ViewerStudies collection for a
// study with this studyInstanceUid
var study = ViewerStudies.findOne({
studyInstanceUid: this.data.studyInstanceUid
});
var displaySetInstanceUid = this.data.displaySetInstanceUid;
data.study = study;
setDisplaySet(data, displaySetInstanceUid, templateData);
});
Template.imageViewerViewport.onDestroyed(function() {
log.info('imageViewerViewport onDestroyed');
// When a viewport element is being destroyed
var element = this.find('.imageViewerViewport');
// Try to stop any currently playing clips
// Otherwise the interval will continuously throw errors
try {
var enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement) {
cornerstoneTools.stopClip(element);
}
} catch (error) {
log.warn(error);
}
// 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({
'ActivateViewport .imageViewerViewport': function(e) {
log.info('imageViewerViewport ActivateViewport');
setActiveViewport(e.currentTarget);
},
'click .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
Session.set('activeViewport', viewportIndex);
},
'CornerstoneToolsMouseDoubleClick .imageViewerViewport, CornerstoneToolsDoubleTap .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
layoutManager.toggleEnlargement(viewportIndex);
}
});