From c2c8a43c3a6df650a136d18338144b96e7c8389e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elo=C3=ADzio=20Salgado?= Date: Tue, 24 Jan 2017 14:35:56 -0200 Subject: [PATCH] Removing ohif-cornerstone as a dependency for ohif-viewerbase (Final step) --- .../client/components/viewer/viewer.js | 10 + OHIFViewer/client/components/viewer/viewer.js | 10 + Packages/ohif-core/main.js | 3 +- Packages/ohif-cornerstone/client/index.js | 4 + .../client/lib/classes/MetadataProvider.js | 257 +++++++++++++++++ .../client/lib/parsingUtils.js | 2 +- Packages/ohif-cornerstone/main.js | 13 + Packages/ohif-cornerstone/namespace.js | 25 ++ Packages/ohif-cornerstone/package.js | 6 + .../imageViewerViewport.js | 65 ++--- .../loadingIndicator/loadingIndicator.js | 43 +-- .../viewer/viewerMain/viewerMain.js | 3 - Packages/ohif-viewerbase/client/index.js | 10 - .../client/lib/StackManager.js | 6 +- .../client/lib/metaDataProvider.js | 266 ------------------ .../lib/setMammogramViewportAlignment.js | 38 +-- .../ohif-viewerbase/client/lib/toolManager.js | 37 ++- Packages/ohif-viewerbase/namespace.js | 8 +- 18 files changed, 436 insertions(+), 370 deletions(-) create mode 100644 Packages/ohif-cornerstone/client/index.js create mode 100644 Packages/ohif-cornerstone/client/lib/classes/MetadataProvider.js rename Packages/{ohif-viewerbase => ohif-cornerstone}/client/lib/parsingUtils.js (98%) create mode 100644 Packages/ohif-cornerstone/main.js create mode 100644 Packages/ohif-cornerstone/namespace.js delete mode 100644 Packages/ohif-viewerbase/client/lib/metaDataProvider.js diff --git a/LesionTracker/client/components/viewer/viewer.js b/LesionTracker/client/components/viewer/viewer.js index ebaf484f5..48fcf2083 100644 --- a/LesionTracker/client/components/viewer/viewer.js +++ b/LesionTracker/client/components/viewer/viewer.js @@ -6,6 +6,7 @@ import { _ } from 'meteor/underscore'; import { $ } from 'meteor/jquery'; import { OHIF } from 'meteor/ohif:core'; +import 'meteor/ohif:cornerstone'; import 'meteor/ohif:viewerbase'; import 'meteor/ohif:metadata'; @@ -15,6 +16,15 @@ Meteor.startup(() => { Session.set('MeasurementsReady', false); OHIF.viewer.stackImagePositionOffsetSynchronizer = new OHIF.viewerbase.StackImagePositionOffsetSynchronizer(); + + // Create the synchronizer used to update reference lines + OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer); + + OHIF.viewer.metadataProvider = OHIF.cornerstone.metadataProvider; + + // Metadata configuration + const metadataProvider = OHIF.viewer.metadataProvider; + cornerstoneTools.metaData.addProvider(metadataProvider.provider.bind(metadataProvider)); }); Template.viewer.onCreated(() => { diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index e4a23f6a0..6a6e1a0ed 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -4,6 +4,7 @@ import { Template } from 'meteor/templating'; import { ReactiveDict } from 'meteor/reactive-dict'; import { OHIF } from 'meteor/ohif:core'; +import 'meteor/ohif:cornerstone'; import 'meteor/ohif:viewerbase'; import 'meteor/ohif:metadata'; @@ -31,6 +32,15 @@ Meteor.startup(() => { }; OHIF.viewer.stackImagePositionOffsetSynchronizer = new OHIF.viewerbase.StackImagePositionOffsetSynchronizer(); + + // Create the synchronizer used to update reference lines + OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer); + + OHIF.viewer.metadataProvider = OHIF.cornerstone.metadataProvider; + + // Metadata configuration + const metadataProvider = OHIF.viewer.metadataProvider; + cornerstoneTools.metaData.addProvider(metadataProvider.provider.bind(metadataProvider)); }); diff --git a/Packages/ohif-core/main.js b/Packages/ohif-core/main.js index 80ee5da3e..c9fb3d920 100644 --- a/Packages/ohif-core/main.js +++ b/Packages/ohif-core/main.js @@ -8,7 +8,8 @@ const OHIF = { log: {}, ui: {}, utils: {}, - viewer: {} + viewer: {}, + cornerstone: {} }; // Expose the OHIF object to the client if it is on development mode diff --git a/Packages/ohif-cornerstone/client/index.js b/Packages/ohif-cornerstone/client/index.js new file mode 100644 index 000000000..63f94473a --- /dev/null +++ b/Packages/ohif-cornerstone/client/index.js @@ -0,0 +1,4 @@ +import { Cornerstone } from '../namespace'; + +import { MetadataProvider } from './lib/classes/MetadataProvider'; +Cornerstone.metadataProvider = new MetadataProvider(); \ No newline at end of file diff --git a/Packages/ohif-cornerstone/client/lib/classes/MetadataProvider.js b/Packages/ohif-cornerstone/client/lib/classes/MetadataProvider.js new file mode 100644 index 000000000..1f0eea99f --- /dev/null +++ b/Packages/ohif-cornerstone/client/lib/classes/MetadataProvider.js @@ -0,0 +1,257 @@ +import { parsingUtils } from '../parsingUtils'; + +export class MetadataProvider { + + constructor() { + this.metadataLookup = {}; + } + + /** + * Cornerstone Metadata provider to store image meta data + * Data from instances, series, and studies are associated with + * imageIds to facilitate usage of this information by Cornerstone's Tools + * + * e.g. the imagePlane metadata object contains instance information about + * row/column pixel spacing, patient position, and patient orientation. It + * is used in CornerstoneTools to position reference lines and orientation markers. + * + * @param {String} imageId The Cornerstone ImageId + * @param {Object} data An object containing instance, series, and study metadata + */ + addMetadata(imageId, data) { + const instanceMetadata = data.instance; + const seriesMetadata = data.series; + const studyMetadata = data.study; + const imageIndex = data.imageIndex; + const numImages = data.numImages; + + const metadata = {}; + + metadata.study = { + patientId: studyMetadata.patientId, + studyInstanceUid: studyMetadata.studyInstanceUid, + studyDate: studyMetadata.studyDate, + studyTime: studyMetadata.studyTime, + studyDescription: studyMetadata.studyDescription, + institutionName: studyMetadata.institutionName, + patientHistory: studyMetadata.patientHistory + }; + + metadata.series = { + seriesDescription: seriesMetadata.seriesDescription, + seriesNumber: seriesMetadata.seriesNumber, + modality: seriesMetadata.modality, + seriesInstanceUid: seriesMetadata.seriesInstanceUid, + numImages: numImages + }; + + metadata.instance = instanceMetadata; + + metadata.patient = { + name: studyMetadata.patientName, + id: studyMetadata.patientId, + birthDate: studyMetadata.patientBirthDate, + sex: studyMetadata.patientSex + }; + + // If there is sufficient information, populate + // the imagePlane object for easier use in the Viewer + metadata.imagePlane = this.getImagePlane(instanceMetadata); + + // Add the metadata to the imageId lookup object + this.metadataLookup[imageId] = metadata; + } + + /** + * Adds a set of metadata to the Cornerstone metadata provider given a specific + * imageId, type, and dataset + * + * @param imageId + * @param type (e.g. series, instance, tagDisplay) + * @param data + */ + addSpecificMetadata(imageId, type, data) { + const metadata = {}; + metadata[type] = data; + + this.metadataLookup[imageId] = $.extend(this.metadataLookup[imageId], metadata); + } + + + getFromDataSet(dataSet, type, tag) { + if (!dataSet) { + return; + } + + const fn = dataSet[type]; + if (!fn) { + return; + } + + return fn.call(dataSet, tag); + } + + /** + * Updates the related metadata for missing fields given a specified image + * + * @param image + */ + updateMetadata(image) { + const imageMetadata = this.metadataLookup[image.imageId]; + if (!imageMetadata) { + return; + } + + imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows; + imageMetadata.instance.columns = imageMetadata.instance.columns || image.columns; + + imageMetadata.instance.sopClassUid = imageMetadata.instance.sopClassUid || this.getFromDataSet(image.data, 'string', 'x00080016'); + imageMetadata.instance.sopInstanceUid = imageMetadata.instance.sopInstanceUid || this.getFromDataSet(image.data, 'string', 'x00080018'); + + imageMetadata.instance.pixelSpacing = imageMetadata.instance.pixelSpacing || this.getFromDataSet(image.data, 'string', 'x00280030'); + imageMetadata.instance.frameOfReferenceUID = imageMetadata.instance.frameOfReferenceUID || this.getFromDataSet(image.data, 'string', 'x00200052'); + imageMetadata.instance.imageOrientationPatient = imageMetadata.instance.imageOrientationPatient || this.getFromDataSet(image.data, 'string', 'x00200037'); + imageMetadata.instance.imagePositionPatient = imageMetadata.instance.imagePositionPatient || this.getFromDataSet(image.data, 'string', 'x00200032'); + + imageMetadata.instance.sliceThickness = imageMetadata.instance.sliceThickness || this.getFromDataSet(image.data, 'string', 'x00180050'); + imageMetadata.instance.sliceLocation = imageMetadata.instance.sliceLocation || this.getFromDataSet(image.data, 'string', 'x00201041'); + imageMetadata.instance.tablePosition = imageMetadata.instance.tablePosition || this.getFromDataSet(image.data, 'string', 'x00189327'); + imageMetadata.instance.spacingBetweenSlices = imageMetadata.instance.spacingBetweenSlices || this.getFromDataSet(image.data, 'string', 'x00180088'); + + imageMetadata.instance.lossyImageCompression = imageMetadata.instance.lossyImageCompression || this.getFromDataSet(image.data, 'string', 'x00282110'); + imageMetadata.instance.lossyImageCompressionRatio = imageMetadata.instance.lossyImageCompressionRatio || this.getFromDataSet(image.data, 'string', 'x00282112'); + + imageMetadata.instance.frameIncrementPointer = imageMetadata.instance.frameIncrementPointer || this.getFromDataSet(image.data, 'string', 'x00280009'); + imageMetadata.instance.frameTime = imageMetadata.instance.frameTime || this.getFromDataSet(image.data, 'string', 'x00181063'); + imageMetadata.instance.frameTimeVector = imageMetadata.instance.frameTimeVector || this.getFromDataSet(image.data, 'string', 'x00181065'); + + if (image.data && !imageMetadata.instance.multiframeMetadata) { + imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(image.data); + } + + imageMetadata.imagePlane = imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance); + } + + /** + * Constructs and returns the imagePlane given the metadata instance + * + * @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane + * @returns imagePlane The constructed imagePlane to be used in viewer easily + */ + getImagePlane(instance) { + if (!instance.rows || !instance.columns || !instance.pixelSpacing || + !instance.frameOfReferenceUID || !instance.imageOrientationPatient || + !instance.imagePositionPatient) { + return; + } + + const imageOrientation = instance.imageOrientationPatient.split('\\'); + const imagePosition = instance.imagePositionPatient.split('\\'); + + let columnPixelSpacing = 1.0; + let rowPixelSpacing = 1.0; + if (instance.pixelSpacing) { + const split = instance.pixelSpacing.split('\\'); + rowPixelSpacing = parseFloat(split[0]); + columnPixelSpacing = parseFloat(split[1]); + } + + return { + frameOfReferenceUID: + instance.frameOfReferenceUID, + rows: + instance.rows, + columns: + instance.columns, + rowCosines: + new cornerstoneMath.Vector3(parseFloat(imageOrientation[0]), parseFloat(imageOrientation[1]), parseFloat(imageOrientation[2])), + columnCosines: + new cornerstoneMath.Vector3(parseFloat(imageOrientation[3]), parseFloat(imageOrientation[4]), parseFloat(imageOrientation[5])), + imagePositionPatient: + new cornerstoneMath.Vector3(parseFloat(imagePosition[0]), parseFloat(imagePosition[1]), parseFloat(imagePosition[2])), + rowPixelSpacing: + rowPixelSpacing, + columnPixelSpacing: + columnPixelSpacing, + }; + } + + /** + * This function extracts miltiframe information from a dicomParser.DataSet object. + * + * @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found. + * @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc). + */ + getMultiframeModuleMetadata(dataSet) { + const imageInfo = { + isMultiframeImage: false, + frameIncrementPointer: null, + numberOfFrames: 0, + frameTime: 0, + frameTimeVector: null, + averageFrameRate: 0 // backwards compatibility only... it might be useless in the future + }; + + let frameTime; + + if (parsingUtils.isValidDataSet(dataSet)) { + + // (0028,0008) = Number of Frames + const numberOfFrames = dataSet.intString('x00280008', -1); + if (numberOfFrames > 0) { + + // set multi-frame image indicator + imageInfo.isMultiframeImage = true; + imageInfo.numberOfFrames = numberOfFrames; + + // (0028,0009) = Frame Increment Pointer + const frameIncrementPointer = parsingUtils.attributeTag(dataSet, 'x00280009') || ''; + + if (frameIncrementPointer === 'x00181065') { + // Frame Increment Pointer points to Frame Time Vector (0018,1065) field + const frameTimeVector = parsingUtils.floatArray(dataSet, 'x00181065'); + if (frameTimeVector instanceof Array && frameTimeVector.length > 0) { + imageInfo.frameIncrementPointer = 'frameTimeVector'; + imageInfo.frameTimeVector = frameTimeVector; + frameTime = frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length; + imageInfo.averageFrameRate = 1000 / frameTime; + } + } else if (frameIncrementPointer === 'x00181063' || frameIncrementPointer === '') { + // Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility). + // Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups) + // but that case is currently not supported. + frameTime = dataSet.floatString('x00181063', -1); + if (frameTime > 0) { + imageInfo.frameIncrementPointer = 'frameTime'; + imageInfo.frameTime = frameTime; + imageInfo.averageFrameRate = 1000 / frameTime; + } + } + + } + + } + + return imageInfo; + } + + /** + * Looks up metadata for Cornerstone Tools given a specified type and imageId + * A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types + * are keys in the stored metadata objects. + * + * @param type + * @param imageId + * @returns {Object} Relevant metadata of the specified type + */ + provider(type, imageId) { + const imageMetadata = this.metadataLookup[imageId]; + if (!imageMetadata) { + return; + } + + if (imageMetadata.hasOwnProperty(type)) { + return imageMetadata[type]; + } + } +}; diff --git a/Packages/ohif-viewerbase/client/lib/parsingUtils.js b/Packages/ohif-cornerstone/client/lib/parsingUtils.js similarity index 98% rename from Packages/ohif-viewerbase/client/lib/parsingUtils.js rename to Packages/ohif-cornerstone/client/lib/parsingUtils.js index 31ea5bea6..0fa7f385d 100644 --- a/Packages/ohif-viewerbase/client/lib/parsingUtils.js +++ b/Packages/ohif-cornerstone/client/lib/parsingUtils.js @@ -1,4 +1,4 @@ -import {dicomParser} from 'meteor/ohif:cornerstone'; +import { dicomParser } from 'meteor/ohif:cornerstone'; /** * A small set of utilities to help parsing DICOM element values. diff --git a/Packages/ohif-cornerstone/main.js b/Packages/ohif-cornerstone/main.js new file mode 100644 index 000000000..1f598b700 --- /dev/null +++ b/Packages/ohif-cornerstone/main.js @@ -0,0 +1,13 @@ +/** + * Import namespace... + */ + +import { OHIF, Cornerstone } from './namespace.js'; + +/** + * Import scripts that will populate the Cornerstone namespace as a side effect only import. This is effectively the public API... + */ + +import './client/'; // which is actually: import './client/index.js'; + +export { OHIF, Cornerstone }; diff --git a/Packages/ohif-cornerstone/namespace.js b/Packages/ohif-cornerstone/namespace.js new file mode 100644 index 000000000..ed3b82ef9 --- /dev/null +++ b/Packages/ohif-cornerstone/namespace.js @@ -0,0 +1,25 @@ +/** + * Import main dependency + */ + +import { OHIF } from 'meteor/ohif:core'; + +/** + * Append Cornerstone namespace + */ + +const Cornerstone = { + ...OHIF.cornerstone +}; + +/** + * Append Cornerstone namespace to OHIF namespace + */ + +OHIF.cornerstone = Cornerstone; + +/** + * Export relevant objects + */ + +export { OHIF, Cornerstone }; diff --git a/Packages/ohif-cornerstone/package.js b/Packages/ohif-cornerstone/package.js index 50fc5592d..6136efe09 100644 --- a/Packages/ohif-cornerstone/package.js +++ b/Packages/ohif-cornerstone/package.js @@ -7,8 +7,12 @@ Package.describe({ Package.onUse(function(api) { api.versionsFrom('1.4'); + api.use('ecmascript'); + api.use('jquery'); + api.use('ohif:core'); + api.addFiles('client/cornerstone.js', 'client', { bare: true }); @@ -42,4 +46,6 @@ Package.onUse(function(api) { api.export('cornerstoneTools', 'client'); api.export('cornerstoneWADOImageLoader', 'client'); api.export('dicomParser', 'client'); + + api.mainModule('main.js', 'client'); }); diff --git a/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js b/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js index b89e4ee20..f41905b28 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js +++ b/Packages/ohif-viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js @@ -14,7 +14,6 @@ import { updateCrosshairsSynchronizer } from '../../../lib/updateCrosshairsSynch import { toolManager } from '../../../lib/toolManager'; import { updateOrientationMarkers } from '../../../lib/updateOrientationMarkers'; import { getInstanceClassDefaultViewport } from '../../../lib/instanceClassSpecificViewport'; -import { updateMetaData } from '../../../lib/metaDataProvider'; import { OHIFError } from '../../../lib/classes/OHIFError'; const allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' + @@ -110,7 +109,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => { // 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; + window.ViewportLoading[viewportIndex] = imageId; // Enable Cornerstone for the viewport element const options = { @@ -159,6 +158,14 @@ const loadDisplaySetIntoViewport = (data, templateData) => { } } + // 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; @@ -188,8 +195,10 @@ const loadDisplaySetIntoViewport = (data, templateData) => { enabledElement.image = image; enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, image); - // Update the metaData for missing fields - updateMetaData(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) { @@ -230,7 +239,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => { // Remove the data for this viewport from the ViewportLoading object // This will stop the loading percentage complete from being displayed. - delete ViewportLoading[viewportIndex]; + delete window.ViewportLoading[viewportIndex]; // Call the handler function that represents the end of the image loading phase // (e.g. hide the progress text box) @@ -330,8 +339,10 @@ const loadDisplaySetIntoViewport = (data, templateData) => { // since this callback function is called multiple times (eg: when a tool is // enabled/disabled -> cornerstone[toolName].tool.enable) - // Update the metaData for missing fields - updateMetaData(eventData.enabledElement.image); + 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 @@ -507,43 +518,9 @@ const setDisplaySet = (data, displaySetInstanceUid, templateData) => { 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 - */ -const getKeysByValue = (object, value) => { - // http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value - return Object.keys(object).filter(key => object[key] === value); -}; - -Meteor.startup(function() { - // On Meteor startup, define the global objects used to store loading imageIds - // by viewport / thumbnail element - ViewportLoading = {}; - - // 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); - }); - - const encodedId = OHIF.string.encodeId(eventData.imageId); - Session.set('CornerstoneThumbnailLoadProgress' + encodedId, eventData.percentComplete); - }); - - const config = { - magnifySize: 300, - magnificationLevel: 3 - }; - - cornerstoneTools.magnify.setConfiguration(config); +Meteor.startup(() => { + window.ViewportLoading = window.ViewportLoading || {}; + toolManager.configureLoadProcess(); }); Template.imageViewerViewport.onRendered(function() { diff --git a/Packages/ohif-viewerbase/client/components/viewer/loadingIndicator/loadingIndicator.js b/Packages/ohif-viewerbase/client/components/viewer/loadingIndicator/loadingIndicator.js index cdba0432e..d20aefcf8 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/loadingIndicator/loadingIndicator.js +++ b/Packages/ohif-viewerbase/client/components/viewer/loadingIndicator/loadingIndicator.js @@ -3,36 +3,41 @@ import { Template } from 'meteor/templating'; import { $ } from 'meteor/jquery'; import { OHIF } from 'meteor/ohif:core'; -Meteor.startup(function() { - cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler); - cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler); - cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler); +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); + } }); -var loadHandlerTimeout; +let loadHandlerTimeout; -const startLoadingHandler = function(element) { +const startLoadingHandler = element => { clearTimeout(loadHandlerTimeout); - loadHandlerTimeout = setTimeout(function() { + loadHandlerTimeout = setTimeout(() => { console.log('startLoading'); - var elem = $(element); + 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 = function(element) { +const doneLoadingHandler = element => { clearTimeout(loadHandlerTimeout); - var elem = $(element); + const elem = $(element); elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none'); elem.find('canvas').not('.magnifyTool').removeClass('faded'); elem.siblings('.imageViewerLoadingIndicator').css('display', 'none'); }; -const errorLoadingHandler = function(element, imageId, error, source) { +const errorLoadingHandler = (element, imageId, error, source) => { clearTimeout(loadHandlerTimeout); - var elem = $(element); + const elem = $(element); // Could probably chain all of these, but this is more readable elem.find('canvas').not('.magnifyTool').removeClass('faded'); @@ -43,14 +48,14 @@ const errorLoadingHandler = function(element, imageId, error, source) { return; } - var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator'); + 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". - var errorDetails = { - network: "A network error has occurred" + const errorDetails = { + network: 'A network error has occurred' // We need to expand this further when we see more obscure error messages }; @@ -58,17 +63,17 @@ const errorLoadingHandler = function(element, imageId, error, source) { error = errorDetails[error]; } - errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + imageId); + errorLoadingIndicator.find('.description').text(`An error has occurred while loading image: ${imageId}`); if (error) { - errorLoadingIndicator.find('.details').text("Details: " + error); + errorLoadingIndicator.find('.details').text(`Details: ${error}`); } }; Template.loadingIndicator.helpers({ 'percentComplete'() { - var percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex); + const percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex); if (percentComplete && percentComplete !== 100) { - return percentComplete + '%'; + return `${percentComplete}%`; } } }); diff --git a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js index ee03ad535..368eaf4a4 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js +++ b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js @@ -10,9 +10,6 @@ import { ResizeViewportManager } from '../../../lib/classes/ResizeViewportManage import { LayoutManager } from '../../../lib/classes/LayoutManager'; Meteor.startup(() => { - // Create the synchronizer used to update reference lines - OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer); - window.ResizeViewportManager = window.ResizeViewportManager || new ResizeViewportManager(); }); diff --git a/Packages/ohif-viewerbase/client/index.js b/Packages/ohif-viewerbase/client/index.js index 17c3edb27..c88b7778f 100644 --- a/Packages/ohif-viewerbase/client/index.js +++ b/Packages/ohif-viewerbase/client/index.js @@ -76,16 +76,6 @@ import { getInstanceClassDefaultViewport, setInstanceClassDefaultViewportFunctio Viewerbase.getInstanceClassDefaultViewport = getInstanceClassDefaultViewport; Viewerbase.setInstanceClassDefaultViewportFunction = setInstanceClassDefaultViewportFunction; -// setMammogramViewportAlignment -import { setMammogramViewportAlignment } from './lib/setMammogramViewportAlignment'; -Viewerbase.setMammogramViewportAlignment = setMammogramViewportAlignment; - -// addMetaData, addSpecificMetadata, updateMetaData -import { addMetaData, addSpecificMetadata, updateMetaData } from './lib/metaDataProvider'; -Viewerbase.addMetaData = addMetaData; -Viewerbase.addSpecificMetadata = addSpecificMetadata; -Viewerbase.updateMetaData = updateMetaData; - // displayReferenceLines import { displayReferenceLines } from './lib/displayReferenceLines'; Viewerbase.displayReferenceLines = displayReferenceLines; diff --git a/Packages/ohif-viewerbase/client/lib/StackManager.js b/Packages/ohif-viewerbase/client/lib/StackManager.js index 39488b194..922b45d8f 100644 --- a/Packages/ohif-viewerbase/client/lib/StackManager.js +++ b/Packages/ohif-viewerbase/client/lib/StackManager.js @@ -1,6 +1,5 @@ import { OHIF } from 'meteor/ohif:core'; import { getImageId } from './getImageId'; -import { addMetaData } from './metaDataProvider'; import { OHIFError } from './classes/OHIFError'; let stackMap = {}; @@ -17,6 +16,7 @@ const stackUpdatedCallbacks = []; * @return {Array} Array with image IDs */ function createAndAddStack(stackMap, study, displaySet) { + const metadataProvider = OHIF.viewer.metadataProvider; const numImages = displaySet.images.length; const imageIds = []; let imageId; @@ -38,13 +38,13 @@ function createAndAddStack(stackMap, study, displaySet) { metaData.frame = i; imageId = getImageId(image, i); imageIds.push(imageId); - addMetaData(imageId, metaData); + metadataProvider.addMetadata(imageId, metaData); } } else { imageId = getImageId(image); imageIds.push(imageId); - addMetaData(imageId, metaData); + metadataProvider.addMetadata(imageId, metaData); } }); diff --git a/Packages/ohif-viewerbase/client/lib/metaDataProvider.js b/Packages/ohif-viewerbase/client/lib/metaDataProvider.js deleted file mode 100644 index cb6ed1834..000000000 --- a/Packages/ohif-viewerbase/client/lib/metaDataProvider.js +++ /dev/null @@ -1,266 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { parsingUtils } from './parsingUtils'; - -const metaDataLookup = {}; - -/** - * Cornerstone MetaData provider to store image meta data - * Data from instances, series, and studies are associated with - * imageIds to facilitate usage of this information by Cornerstone's Tools - * - * e.g. the imagePlane metaData object contains instance information about - * row/column pixel spacing, patient position, and patient orientation. It - * is used in CornerstoneTools to position reference lines and orientation markers. - * - * @param {String} imageId The Cornerstone ImageId - * @param {Object} data An object containing instance, series, and study metaData - */ -const addMetaData = (imageId, data) => { - let instanceMetaData = data.instance; - let seriesMetaData = data.series; - let studyMetaData = data.study; - let imageIndex = data.imageIndex; - let numImages = data.numImages; - - let metaData = {}; - - metaData.study = { - patientId: studyMetaData.patientId, - studyInstanceUid: studyMetaData.studyInstanceUid, - studyDate: studyMetaData.studyDate, - studyTime: studyMetaData.studyTime, - studyDescription: studyMetaData.studyDescription, - institutionName: studyMetaData.institutionName, - patientHistory: studyMetaData.patientHistory - }; - - metaData.series = { - seriesDescription: seriesMetaData.seriesDescription, - seriesNumber: seriesMetaData.seriesNumber, - modality: seriesMetaData.modality, - seriesInstanceUid: seriesMetaData.seriesInstanceUid, - numImages: numImages - }; - - metaData.instance = instanceMetaData; - - metaData.patient = { - name: studyMetaData.patientName, - id: studyMetaData.patientId, - birthDate: studyMetaData.patientBirthDate, - sex: studyMetaData.patientSex - }; - - // If there is sufficient information, populate - // the imagePlane object for easier use in the Viewer - metaData.imagePlane = getImagePlane(instanceMetaData); - - // Add the metaData to the imageId lookup object - metaDataLookup[imageId] = metaData; -}; - -/** - * Adds a set of metaData to the Cornerstone metaData provider given a specific - * imageId, type, and dataset - * - * @param imageId - * @param type (e.g. series, instance, tagDisplay) - * @param data - */ -const addSpecificMetadata = (imageId, type, data) => { - let metaData = {}; - metaData[type] = data; - - metaDataLookup[imageId] = $.extend(metaDataLookup[imageId], metaData); -}; - - -function getFromDataSet(dataSet, type, tag) { - if (!dataSet) { - return; - } - - const fn = dataSet[type]; - if (!fn) { - return; - } - - return fn.call(dataSet, tag); -} - -/** - * Updates the related metaData for missing fields given a specified image - * - * @param image - */ -const updateMetaData = image => { - const imageMetaData = metaDataLookup[image.imageId]; - if (!imageMetaData) { - return; - } - - imageMetaData.instance.rows = imageMetaData.instance.rows || image.rows; - imageMetaData.instance.columns = imageMetaData.instance.columns || image.columns; - - imageMetaData.instance.sopClassUid = imageMetaData.instance.sopClassUid || getFromDataSet(image.data, 'string', 'x00080016'); - imageMetaData.instance.sopInstanceUid = imageMetaData.instance.sopInstanceUid || getFromDataSet(image.data, 'string', 'x00080018'); - - imageMetaData.instance.pixelSpacing = imageMetaData.instance.pixelSpacing || getFromDataSet(image.data, 'string', 'x00280030'); - imageMetaData.instance.frameOfReferenceUID = imageMetaData.instance.frameOfReferenceUID || getFromDataSet(image.data, 'string', 'x00200052'); - imageMetaData.instance.imageOrientationPatient = imageMetaData.instance.imageOrientationPatient || getFromDataSet(image.data, 'string', 'x00200037'); - imageMetaData.instance.imagePositionPatient = imageMetaData.instance.imagePositionPatient || getFromDataSet(image.data, 'string', 'x00200032'); - - imageMetaData.instance.sliceThickness = imageMetaData.instance.sliceThickness || getFromDataSet(image.data, 'string', 'x00180050'); - imageMetaData.instance.sliceLocation = imageMetaData.instance.sliceLocation || getFromDataSet(image.data, 'string', 'x00201041'); - imageMetaData.instance.tablePosition = imageMetaData.instance.tablePosition || getFromDataSet(image.data, 'string', 'x00189327'); - imageMetaData.instance.spacingBetweenSlices = imageMetaData.instance.spacingBetweenSlices || getFromDataSet(image.data, 'string', 'x00180088'); - - imageMetaData.instance.lossyImageCompression = imageMetaData.instance.lossyImageCompression || getFromDataSet(image.data, 'string', 'x00282110'); - imageMetaData.instance.lossyImageCompressionRatio = imageMetaData.instance.lossyImageCompressionRatio || getFromDataSet(image.data, 'string', 'x00282112'); - - imageMetaData.instance.frameIncrementPointer = imageMetaData.instance.frameIncrementPointer || getFromDataSet(image.data, 'string', 'x00280009'); - imageMetaData.instance.frameTime = imageMetaData.instance.frameTime || getFromDataSet(image.data, 'string', 'x00181063'); - imageMetaData.instance.frameTimeVector = imageMetaData.instance.frameTimeVector || getFromDataSet(image.data, 'string', 'x00181065'); - - if (image.data && !imageMetaData.instance.multiframeMetadata) { - imageMetaData.instance.multiframeMetadata = getMultiframeModuleMetaData(image.data); - } - - imageMetaData.imagePlane = imageMetaData.imagePlane || getImagePlane(imageMetaData.instance); -}; - -/** - * Constructs and returns the imagePlane given the metadata instance - * - * @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane - * @returns imagePlane The constructed imagePlane to be used in viewer easily - */ -function getImagePlane(instance) { - if (!instance.rows || !instance.columns || !instance.pixelSpacing || - !instance.frameOfReferenceUID || !instance.imageOrientationPatient || - !instance.imagePositionPatient) { - return; - } - - const imageOrientation = instance.imageOrientationPatient.split('\\'); - const imagePosition = instance.imagePositionPatient.split('\\'); - - let columnPixelSpacing = 1.0; - let rowPixelSpacing = 1.0; - if (instance.pixelSpacing) { - const split = instance.pixelSpacing.split('\\'); - rowPixelSpacing = parseFloat(split[0]); - columnPixelSpacing = parseFloat(split[1]); - } - - return { - frameOfReferenceUID: - instance.frameOfReferenceUID, - rows: - instance.rows, - columns: - instance.columns, - rowCosines: - new cornerstoneMath.Vector3(parseFloat(imageOrientation[0]), parseFloat(imageOrientation[1]), parseFloat(imageOrientation[2])), - columnCosines: - new cornerstoneMath.Vector3(parseFloat(imageOrientation[3]), parseFloat(imageOrientation[4]), parseFloat(imageOrientation[5])), - imagePositionPatient: - new cornerstoneMath.Vector3(parseFloat(imagePosition[0]), parseFloat(imagePosition[1]), parseFloat(imagePosition[2])), - rowPixelSpacing: - rowPixelSpacing, - columnPixelSpacing: - columnPixelSpacing, - }; -} - -/** - * This function extracts miltiframe information from a dicomParser.DataSet object. - * - * @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found. - * @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc). - */ -function getMultiframeModuleMetaData(dataSet) { - - let numberOfFrames, - frameIncrementPointer, - frameTime, - frameTimeVector, - imageInfo = { - isMultiframeImage: false, - frameIncrementPointer: null, - numberOfFrames: 0, - frameTime: 0, - frameTimeVector: null, - averageFrameRate: 0 // backwards compatibility only... it might be useless in the future - }; - - if (parsingUtils.isValidDataSet(dataSet)) { - - // (0028,0008) = Number of Frames - numberOfFrames = dataSet.intString('x00280008', -1); - if (numberOfFrames > 0) { - - // set multi-frame image indicator - imageInfo.isMultiframeImage = true; - imageInfo.numberOfFrames = numberOfFrames; - - // (0028,0009) = Frame Increment Pointer - frameIncrementPointer = parsingUtils.attributeTag(dataSet, 'x00280009') || ''; - - if (frameIncrementPointer === 'x00181065') { - // Frame Increment Pointer points to Frame Time Vector (0018,1065) field - frameTimeVector = parsingUtils.floatArray(dataSet, 'x00181065'); - if (frameTimeVector instanceof Array && frameTimeVector.length > 0) { - imageInfo.frameIncrementPointer = 'frameTimeVector'; - imageInfo.frameTimeVector = frameTimeVector; - frameTime = frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length; - imageInfo.averageFrameRate = 1000 / frameTime; - } - } else if (frameIncrementPointer === 'x00181063' || frameIncrementPointer === '') { - // Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility). - // Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups) - // but that case is currently not supported. - frameTime = dataSet.floatString('x00181063', -1); - if (frameTime > 0) { - imageInfo.frameIncrementPointer = 'frameTime'; - imageInfo.frameTime = frameTime; - imageInfo.averageFrameRate = 1000 / frameTime; - } - } - - } - - } - - return imageInfo; -} - -/** - * Looks up metaData for Cornerstone Tools given a specified type and imageId - * A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types - * are keys in the stored metaData objects. - * - * @param type - * @param imageId - * @returns {Object} Relevant metaData of the specified type - */ -function provider(type, imageId) { - let imageMetaData = metaDataLookup[imageId]; - if (!imageMetaData) { - return; - } - - if (imageMetaData.hasOwnProperty(type)) { - return imageMetaData[type]; - } -} - -Meteor.startup(function() { - cornerstoneTools.metaData.addProvider(provider); -}); - -/** - * Export relevant symbols - */ - -export { addMetaData, addSpecificMetadata, updateMetaData }; diff --git a/Packages/ohif-viewerbase/client/lib/setMammogramViewportAlignment.js b/Packages/ohif-viewerbase/client/lib/setMammogramViewportAlignment.js index c19c80dfe..75989dc2f 100644 --- a/Packages/ohif-viewerbase/client/lib/setMammogramViewportAlignment.js +++ b/Packages/ohif-viewerbase/client/lib/setMammogramViewportAlignment.js @@ -1,24 +1,28 @@ import { Meteor } from 'meteor/meteor'; import { $ } from 'meteor/jquery'; + +import { OHIF } from 'meteor/ohif:core'; import { setInstanceClassDefaultViewportFunction } from './instanceClassSpecificViewport'; -import { addSpecificMetadata } from './metaDataProvider'; const setMammogramViewportAlignment = (series, enabledElement, imageId) => { // Don't apply the MG viewport alignment to other series types - let viewTypes = ['MLO', 'CC', 'LM', 'ML', 'XCCL']; - let requiresInversion = ['LM']; // Tomo series are flipped + const viewTypes = ['MLO', 'CC', 'LM', 'ML', 'XCCL']; + const requiresInversion = ['LM']; // Tomo series are flipped - let instance = cornerstoneTools.metaData.get('instance', imageId); + const instance = cornerstoneTools.metaData.get('instance', imageId); if (!instance) { return; } - let laterality = instance.laterality; - let element = enabledElement.element; - let position; + const element = enabledElement.element; - let left = $(enabledElement.canvas).offset().left; - let right = left + enabledElement.canvas.width; + const left = $(enabledElement.canvas).offset().left; + const right = left + enabledElement.canvas.width; + + const metadataProvider = OHIF.viewer.metadataProvider; + + let laterality = instance.laterality; + let position; if (viewTypes.indexOf(instance.viewPosition) < 0) { return; @@ -26,14 +30,14 @@ const setMammogramViewportAlignment = (series, enabledElement, imageId) => { // Check if we should flip the laterality if (requiresInversion.indexOf(instance.viewPosition) > -1) { - if (laterality === "R") { - laterality = "L"; - } else if (laterality === "L") { - laterality = "R"; + if (laterality === 'R') { + laterality = 'L'; + } else if (laterality === 'L') { + laterality = 'R'; } } - if (laterality === "R") { + if (laterality === 'R') { // Set X translation to Canvas max in image pixels - image width // This places it on the right side of the screen position = cornerstone.pageToPixel(element, right, 0); @@ -41,7 +45,7 @@ const setMammogramViewportAlignment = (series, enabledElement, imageId) => { enabledElement.viewport.translation.x += position.x - enabledElement.image.width; } - addSpecificMetadata(imageId, 'tagDisplay', { + metadataProvider.addSpecificMetadata(imageId, 'tagDisplay', { side: 'L' }); @@ -53,7 +57,7 @@ const setMammogramViewportAlignment = (series, enabledElement, imageId) => { enabledElement.viewport.translation.x += position.x; } - addSpecificMetadata(imageId, 'tagDisplay', { + metadataProvider.addSpecificMetadata(imageId, 'tagDisplay', { side: 'R' }); } @@ -62,7 +66,7 @@ const setMammogramViewportAlignment = (series, enabledElement, imageId) => { }; Meteor.startup(function() { - setInstanceClassDefaultViewportFunction("1.2.840.10008.5.1.4.1.1.1.2", setMammogramViewportAlignment); + setInstanceClassDefaultViewportFunction('1.2.840.10008.5.1.4.1.1.1.2', setMammogramViewportAlignment); }); export { setMammogramViewportAlignment }; diff --git a/Packages/ohif-viewerbase/client/lib/toolManager.js b/Packages/ohif-viewerbase/client/lib/toolManager.js index 57c971aa9..8ca1bf400 100644 --- a/Packages/ohif-viewerbase/client/lib/toolManager.js +++ b/Packages/ohif-viewerbase/client/lib/toolManager.js @@ -122,7 +122,8 @@ export const toolManager = { configureTools() { // Get Cornerstone Tools const { panMultiTouch, textStyle, toolStyle, toolColors, - length, arrowAnnotate, zoom, ellipticalRoi } = cornerstoneTools; + length, arrowAnnotate, zoom, ellipticalRoi, + textMarker, magnify } = cornerstoneTools; // Set the configuration for the multitouch pan tool const multiTouchPanConfig = { @@ -187,7 +188,7 @@ export const toolManager = { shadowOffsetX: shadowConfig.shadowOffsetX, shadowOffsetY: shadowConfig.shadowOffsetY }; - cornerstoneTools.textMarker.setConfiguration(textMarkerConfig); + textMarker.setConfiguration(textMarkerConfig); // Set the configuration values for the text annotation (Arrow) tool const annotateConfig = { @@ -203,6 +204,38 @@ export const toolManager = { maxScale: 10 }; zoom.setConfiguration(zoomConfig); + + const magnifyConfig = { + magnifySize: 300, + magnificationLevel: 3 + }; + magnify.setConfiguration(magnifyConfig); + }, + /** + * 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 + */ + getKeysByValue(object, value) { + // http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value + return Object.keys(object).filter(key => object[key] === value); + }, + configureLoadProcess() { + // 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', (e, eventData) => { + viewportIndices = this.getKeysByValue(window.ViewportLoading, eventData.imageId); + viewportIndices.forEach(viewportIndex => { + Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete); + }); + + const encodedId = OHIF.string.encodeId(eventData.imageId); + Session.set('CornerstoneThumbnailLoadProgress' + encodedId, eventData.percentComplete); + }); }, setGestures(newGestures) { gestures = newGestures; diff --git a/Packages/ohif-viewerbase/namespace.js b/Packages/ohif-viewerbase/namespace.js index 6f4174385..21e3568a0 100644 --- a/Packages/ohif-viewerbase/namespace.js +++ b/Packages/ohif-viewerbase/namespace.js @@ -1,23 +1,23 @@ /** - * Import main dependency... + * Import main dependency */ import { OHIF } from 'meteor/ohif:core'; /** - * Create Viewerbase namespace... + * Create Viewerbase namespace */ const Viewerbase = {}; /** - * Append Viewerbase namespace to OHIF namespace... + * Append Viewerbase namespace to OHIF namespace */ OHIF.viewerbase = Viewerbase; /** - * Export relevant objects... + * Export relevant objects */ export { OHIF, Viewerbase };