From a8626d02c08e02f06a4b0d415fc943d1954e1c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elo=C3=ADzio=20Salgado?= Date: Wed, 18 Jan 2017 13:59:03 -0200 Subject: [PATCH] toolManager: fixed annotate tool issues and added Text Marker Tool (importing callback functions from Nucleus) and using Metadata classes for OHIF Viewer --- OHIFViewer/client/components/viewer/viewer.js | 6 +- .../client/cornerstoneWADOImageLoader.js | 76 ++++---- .../client/protocolEngine.js | 1 - .../lib/studylist/openNewTabWithTimepoint.js | 2 +- .../lib/studylist/studylistModification.js | 8 +- .../longitudinalStudyListContextMenu.js | 8 +- .../client/lib/jumpToRowItem.js | 1 - .../ohif-study-list/client/lib/openNewTab.js | 8 +- .../ohif-study-list/client/lib/switchToTab.js | 34 ++-- .../server/services/dimse/retrieveMetadata.js | 4 +- .../services/remote/retrieveMetadata.js | 4 +- .../server/services/wado/retrieveMetadata.js | 2 + .../studySeriesQuickSwitch.js | 1 - .../studyTimepointBrowser/studyTimepoint.js | 1 - Packages/ohif-viewerbase/client/index.js | 8 + .../client/lib/StackManager.js | 181 +++++++++--------- .../client/lib/annotateTextUtils.js | 122 ++++++++++++ .../client/lib/classes/LayoutManager.js | 19 +- .../client/lib/createStacks.js | 121 ++++++------ .../ohif-viewerbase/client/lib/getImageId.js | 8 +- .../client/lib/getWADORSImageId.js | 20 +- .../client/lib/sortingManager.js | 4 +- .../client/lib/textMarkerUtils.js | 87 +++++++++ .../ohif-viewerbase/client/lib/toolManager.js | 32 +++- 24 files changed, 516 insertions(+), 242 deletions(-) create mode 100644 Packages/ohif-viewerbase/client/lib/annotateTextUtils.js create mode 100644 Packages/ohif-viewerbase/client/lib/textMarkerUtils.js diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index 44548ccc7..0d906ffd8 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -55,9 +55,9 @@ Template.viewer.onCreated(() => { ViewerData[contentId].studyInstanceUids = []; instance.data.studies.forEach(study => { study.selected = true; - study.displaySets = OHIF.viewerbase.createStacks(study); - // const studyMetadata = new OHIF.metadata.StudyMetadata(study); - // study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); + // study.displaySets = OHIF.viewerbase.createStacks(study); + const studyMetadata = new OHIF.metadata.StudyMetadata(study); + study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); OHIF.viewer.Studies.insert(study); ViewerData[contentId].studyInstanceUids.push(study.studyInstanceUid); }); diff --git a/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js index 8c7f0f636..ebf893912 100644 --- a/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js +++ b/Packages/ohif-cornerstone/client/cornerstoneWADOImageLoader.js @@ -2715,37 +2715,37 @@ var JpegImage = (function jpegImage() { -// Huffman table for fast search: (HuffTab) 8-bit Look up table 2-layer search architecture, 1st-layer represent 256 node (8 bits) if codeword-length > 8 -// bits, then the entry of 1st-layer = (# of 2nd-layer table) | MSB and it is stored in the 2nd-layer Size of tables in each layer are 256. -// HuffTab[*][*][0-256] is always the only 1st-layer table. +// Huffman table for fast search: (HuffTab) 8-bit Look up table 2-layer search architecture, 1st-layer represent 256 node (8 bits) if codeword-length > 8 +// bits, then the entry of 1st-layer = (# of 2nd-layer table) | MSB and it is stored in the 2nd-layer Size of tables in each layer are 256. +// HuffTab[*][*][0-256] is always the only 1st-layer table. // -// An entry can be: (1) (# of 2nd-layer table) | MSB , for code length > 8 in 1st-layer (2) (Code length) << 8 | HuffVal +// An entry can be: (1) (# of 2nd-layer table) | MSB , for code length > 8 in 1st-layer (2) (Code length) << 8 | HuffVal // -// HuffmanValue(table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...) -// ): -// return: Huffman Value of table -// 0xFF?? if it receives a MARKER -// Parameter: table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...) -// temp temp storage for remainded bits -// index index to bit of temp -// in FILE pointer -// Effect: -// temp store new remainded bits -// index change to new index -// in change to new position -// NOTE: -// Initial by temp=0; index=0; -// NOTE: (explain temp and index) -// temp: is always in the form at calling time or returning time -// | byte 4 | byte 3 | byte 2 | byte 1 | -// | 0 | 0 | 00000000 | 00000??? | if not a MARKER -// ^index=3 (from 0 to 15) -// 321 -// NOTE (marker and marker_index): -// If get a MARKER from 'in', marker=the low-byte of the MARKER -// and marker_index=9 -// If marker_index=9 then index is always > 8, or HuffmanValue() -// will not be called +// HuffmanValue(table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...) +// ): +// return: Huffman Value of table +// 0xFF?? if it receives a MARKER +// Parameter: table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...) +// temp temp storage for remainded bits +// index index to bit of temp +// in FILE pointer +// Effect: +// temp store new remainded bits +// index change to new index +// in change to new position +// NOTE: +// Initial by temp=0; index=0; +// NOTE: (explain temp and index) +// temp: is always in the form at calling time or returning time +// | byte 4 | byte 3 | byte 2 | byte 1 | +// | 0 | 0 | 00000000 | 00000??? | if not a MARKER +// ^index=3 (from 0 to 15) +// 321 +// NOTE (marker and marker_index): +// If get a MARKER from 'in', marker=the low-byte of the MARKER +// and marker_index=9 +// If marker_index=9 then index is always > 8, or HuffmanValue() +// will not be called jpeg.lossless.Decoder.prototype.getHuffmanValue = function (table, temp, index) { /*jslint bitwise: true */ @@ -3302,13 +3302,13 @@ var JpegImage = (function jpegImage() { -// Build_HuffTab() -// Parameter: t table ID -// c table class ( 0 for DC, 1 for AC ) -// L[i] # of codewords which length is i -// V[i][j] Huffman Value (length=i) -// Effect: -// build up HuffTab[t][c] using L and V. +// Build_HuffTab() +// Parameter: t table ID +// c table class ( 0 for DC, 1 for AC ) +// L[i] # of codewords which length is i +// V[i][j] Huffman Value (length=i) +// Effect: +// build up HuffTab[t][c] using L and V. jpeg.lossless.HuffmanTable.prototype.buildHuffTable = function(tab, L, V) { /*jslint bitwise: true */ @@ -4575,7 +4575,7 @@ var JpegImage = (function jpegImage() { // https://groups.google.com/forum/#!searchin/comp.protocols.dicom/Modality$20LUT$20XA/comp.protocols.dicom/UBxhOZ2anJ0/D0R_QP8V2wIJ var sopClassUid = dataSet.string('x00080016'); return sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.1' && // XA - sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.2.1 '; // XRF + sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.2.1 '; // XRF } function makeGrayscaleImage(imageId, dataSet, frame, sharedCacheKey) { @@ -4773,4 +4773,4 @@ var JpegImage = (function jpegImage() { } cornerstoneWADOImageLoader.internal.xhrRequest = xhrRequest; -}($, cornerstone, cornerstoneWADOImageLoader)); +}($, cornerstone, cornerstoneWADOImageLoader)); \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/client/protocolEngine.js b/Packages/ohif-hanging-protocols/client/protocolEngine.js index 58cba49b3..13cb77cba 100644 --- a/Packages/ohif-hanging-protocols/client/protocolEngine.js +++ b/Packages/ohif-hanging-protocols/client/protocolEngine.js @@ -404,7 +404,6 @@ HP.ProtocolEngine = class ProtocolEngine { } // @TypeSafeStudies - var alreadyLoaded = OHIF.viewer.Studies.findBy({ studyInstanceUid: priorStudy.studyInstanceUid }); diff --git a/Packages/ohif-lesiontracker/client/lib/studylist/openNewTabWithTimepoint.js b/Packages/ohif-lesiontracker/client/lib/studylist/openNewTabWithTimepoint.js index 608de6c8d..75e1391a3 100644 --- a/Packages/ohif-lesiontracker/client/lib/studylist/openNewTabWithTimepoint.js +++ b/Packages/ohif-lesiontracker/client/lib/studylist/openNewTabWithTimepoint.js @@ -30,7 +30,7 @@ OHIF.lesiontracker.openNewTabWithTimepoint = (timepointId, title) => { // Update the ViewerData global object ViewerData[contentId] = { title: title, - contentid: contentId, + contentId: contentId, studyInstanceUids: data.studyInstanceUids, timepointIds: data.timepointIds, currentTimepointId: timepointId diff --git a/Packages/ohif-lesiontracker/client/lib/studylist/studylistModification.js b/Packages/ohif-lesiontracker/client/lib/studylist/studylistModification.js index e172606a5..2ecc2b964 100644 --- a/Packages/ohif-lesiontracker/client/lib/studylist/studylistModification.js +++ b/Packages/ohif-lesiontracker/client/lib/studylist/studylistModification.js @@ -42,18 +42,18 @@ function dblClickOnStudy(data) { * @param title The title to be used for the tab heading */ function open(studyInstanceUid, title) { - const contentid = 'viewerTab'; + const contentId = 'viewerTab'; ViewerData = window.ViewerData || ViewerData; // Update the ViewerData global object - ViewerData[contentid] = { + ViewerData[contentId] = { title: title, - contentid: contentid, + contentId: contentId, isUnassociatedStudy: true, studyInstanceUids: [studyInstanceUid] }; // Switch to the new tab - switchToTab(contentid); + switchToTab(contentId); } diff --git a/Packages/ohif-measurements/client/components/longitudinal/longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.js b/Packages/ohif-measurements/client/components/longitudinal/longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.js index c7cbe094f..6e50e4bc1 100644 --- a/Packages/ohif-measurements/client/components/longitudinal/longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.js +++ b/Packages/ohif-measurements/client/components/longitudinal/longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.js @@ -135,17 +135,17 @@ function viewStudies() { const title = selectedStudies[0].patientName; const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid); - const contentid = 'viewerTab'; + const contentId = 'viewerTab'; ViewerData = window.ViewerData || ViewerData; // Update the ViewerData global object - ViewerData[contentid] = { + ViewerData[contentId] = { title: title, - contentid: contentid, + contentId: contentId, studyInstanceUids: studyInstanceUids }; // Switch to the new tab - switchToTab(contentid); + switchToTab(contentId); } diff --git a/Packages/ohif-measurements/client/lib/jumpToRowItem.js b/Packages/ohif-measurements/client/lib/jumpToRowItem.js index c7561ca36..7cf39efdc 100644 --- a/Packages/ohif-measurements/client/lib/jumpToRowItem.js +++ b/Packages/ohif-measurements/client/lib/jumpToRowItem.js @@ -32,7 +32,6 @@ function findAndRenderDisplaySet(displaySets, viewportIndex, studyInstanceUid, s function renderIntoViewport(viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback) { // @TypeSafeStudies - // First, check if we already have this study loaded const alreadyLoadedStudy = OHIF.viewer.Studies.findBy({ studyInstanceUid }); diff --git a/Packages/ohif-study-list/client/lib/openNewTab.js b/Packages/ohif-study-list/client/lib/openNewTab.js index 9ec64f6d5..8c4841555 100644 --- a/Packages/ohif-study-list/client/lib/openNewTab.js +++ b/Packages/ohif-study-list/client/lib/openNewTab.js @@ -11,16 +11,16 @@ openNewTab = function(studyInstanceUid, title) { // Generate a unique ID to represent this tab // We can't just use the Mongo entry ID because // then it will change after hot-reloading. - var contentid = Random.id(); + var contentId = Random.id(); // Update the ViewerData global object ViewerData = window.ViewerData || ViewerData; - ViewerData[contentid] = { + ViewerData[contentId] = { title: title, - contentid: contentid, + contentId: contentId, studyInstanceUids: [studyInstanceUid] }; // Switch to the new tab - switchToTab(contentid); + switchToTab(contentId); }; diff --git a/Packages/ohif-study-list/client/lib/switchToTab.js b/Packages/ohif-study-list/client/lib/switchToTab.js index 72d53f0f4..0986d3e45 100644 --- a/Packages/ohif-study-list/client/lib/switchToTab.js +++ b/Packages/ohif-study-list/client/lib/switchToTab.js @@ -1,3 +1,8 @@ +import { Blaze } from 'meteor/blaze'; +import { Session } from 'meteor/session'; +import { jQuery, $ } from 'meteor/jquery'; +import { Template } from 'meteor/templating'; + import { OHIF } from 'meteor/ohif:core'; /** @@ -33,7 +38,7 @@ switchToTab = function(contentId) { // If we are switching to the StudyList tab, reset any CSS styles // that have been applied to prevent scrolling in the Viewer. // Then stop here, since nothing needs to be re-rendered. - var container; + let container; if (contentId === 'studylistTab') { container = $('.tab-content').find('#studylistTab').get(0); @@ -41,7 +46,7 @@ switchToTab = function(contentId) { return; } - var studylistContainer = document.createElement('div'); + const studylistContainer = document.createElement('div'); studylistContainer.classList.add('studylistContainer'); container.appendChild(studylistContainer); @@ -61,14 +66,14 @@ switchToTab = function(contentId) { return; } - var container = $('.tab-content').find('#viewerTab').get(0); + container = $('.tab-content').find('#viewerTab').get(0); container.innerHTML = ''; // Use Blaze to render the Loading Template into the container // viewStudiesInTab will clear this container Blaze.renderWithData(Template.loadingText, {}, container); - var studies = ViewerData[contentId].studies; + const studies = ViewerData[contentId].studies; if (studies) { // ViewerData already has the meta data (in cases when studylist is launched externally) @@ -76,7 +81,7 @@ switchToTab = function(contentId) { } else { // Use the stored ViewerData global object to retrieve the studyInstanceUid // related to this tab - var studyInstanceUids = ViewerData[contentId].studyInstanceUids; + const studyInstanceUids = ViewerData[contentId].studyInstanceUids; // Attempt to retrieve the meta data (it might be cached) OHIF.studylist.getStudiesMetadata(studyInstanceUids, function(studies) { @@ -85,7 +90,7 @@ switchToTab = function(contentId) { } }; -function viewStudiesInTab(contentId, studies) { +const viewStudiesInTab = (contentId, studies) => { // Tab closed while study data was being retrieved, stop here if (!ViewerData[contentId]) { OHIF.log.warn('Tab closed while study data was being retrieved'); @@ -94,8 +99,11 @@ function viewStudiesInTab(contentId, studies) { // Once we have the study data, store it in a structure with // any other saved data about this tab (e.g. layout structure) - var data = jQuery.extend({}, ViewerData[contentId]); + const data = jQuery.extend({}, ViewerData[contentId]); data.studies = studies; + + // TODO: check if this is necessary. Since the typo bug + // was fixed (it was contentid instead of contentId) data.contentId = contentId; if (ViewerData[contentId].studies && ViewerData[contentId].studies.length) { @@ -103,8 +111,8 @@ function viewStudiesInTab(contentId, studies) { } // Add additional metadata to our study from the studylist - data.studies.forEach(function(study) { - var studylistStudy = StudyListStudies.findOne({ + data.studies.forEach(study => { + const studylistStudy = StudyListStudies.findOne({ studyInstanceUid: study.studyInstanceUid }); @@ -117,10 +125,10 @@ function viewStudiesInTab(contentId, studies) { // Get tab content container given the contentId string // If no such container exists, stop here because something is wrong - var container = $('.tab-content').find('#viewerTab').get(0); + const container = $('.tab-content').find('#viewerTab').get(0); // Remove the loading text template that is inside the tab container by default - var viewerContainer = document.createElement('div'); + const viewerContainer = document.createElement('div'); viewerContainer.classList.add('viewerContainer'); container.innerHTML = ''; container.appendChild(viewerContainer); @@ -129,7 +137,7 @@ function viewStudiesInTab(contentId, studies) { Blaze.renderWithData(Template.viewer, data, viewerContainer); // Retrieve the DOM element of the viewer - var imageViewer = $('#viewer'); + const imageViewer = $('#viewer'); // If it is present in the DOM (it should be), then apply // styles to prevent page scrolling and overscrolling on mobile devices @@ -142,4 +150,4 @@ function viewStudiesInTab(contentId, studies) { // Prevent overscroll on mobile devices document.body.style.position = 'fixed'; } -} +}; diff --git a/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js b/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js index 1c2d9f0bc..03e4a8cf6 100755 --- a/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js +++ b/Packages/ohif-study-list/server/services/dimse/retrieveMetadata.js @@ -127,7 +127,9 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) { derivationDescription: instance[0x00282111], lossyImageCompressionRatio: instance[0x00282112], lossyImageCompressionMethod: instance[0x00282114], - spacingBetweenSlices: instance[0x00180088] + spacingBetweenSlices: instance[0x00180088], + echoNumber: instance[0x00180086], + contrastBolusAgent: instance[0x00180010] }; // Retrieve the actual data over WADO-URI diff --git a/Packages/ohif-study-list/server/services/remote/retrieveMetadata.js b/Packages/ohif-study-list/server/services/remote/retrieveMetadata.js index b999992cf..452131783 100755 --- a/Packages/ohif-study-list/server/services/remote/retrieveMetadata.js +++ b/Packages/ohif-study-list/server/services/remote/retrieveMetadata.js @@ -100,7 +100,9 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { numberOfFrames: parseFloat(remoteGetValue(instance['0028,0008'])), frameIncrementPointer: remoteGetValue(instance['0028,0009']), frameTime: parseFloat(remoteGetValue(instance['0018,1063'])), - frameTimeVector: parseFloatArray(remoteGetValue(instance['0018,1065'])) + frameTimeVector: parseFloatArray(remoteGetValue(instance['0018,1065'])), + echoNumber: remoteGetValue(instance['0018,0086']), + contrastBolusAgent: remoteGetValue(instance['0018,0010']) }; var iid = instance['xxxx,0001'].Value; diff --git a/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js b/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js index 812a2be96..1b53e3e43 100644 --- a/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js +++ b/Packages/ohif-study-list/server/services/wado/retrieveMetadata.js @@ -118,6 +118,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) { derivationDescription: DICOMWeb.getString(instance['00282111']), lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']), lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']), + echoNumber: DICOMWeb.getString(instance['00180086']), + contrastBolusAgent: DICOMWeb.getString(instance['00180010']) }; if (server.imageRendering === 'wadouri') { diff --git a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js index cf717a74f..9502ceccf 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js +++ b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js @@ -29,7 +29,6 @@ Template.studySeriesQuickSwitch.onCreated(() => { const viewportData = instance.getViewportData(viewportIndex); // @TypeSafeStudies - if (viewportData) { // Finds the current study and return it instance.study = OHIF.viewer.Studies.findBy({ diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js index 90e5ad2b5..9dca64c32 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js +++ b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js @@ -41,7 +41,6 @@ Template.studyTimepoint.events({ let $studiesTarget = instance.$('.studyTimepoint'); // @TypeSafeStudies - if (changed.isQuickSwitch) { // Changes the current quick switch study const study = OHIF.viewer.Studies.findBy({ diff --git a/Packages/ohif-viewerbase/client/index.js b/Packages/ohif-viewerbase/client/index.js index d8b948931..6e53dfe15 100644 --- a/Packages/ohif-viewerbase/client/index.js +++ b/Packages/ohif-viewerbase/client/index.js @@ -139,6 +139,14 @@ Viewerbase.sortingManager = sortingManager; import { crosshairsSynchronizers } from './lib/crosshairsSynchronizers'; Viewerbase.crosshairsSynchronizers = crosshairsSynchronizers; +// annotateTextUtils.* +import { annotateTextUtils } from './lib/annotateTextUtils'; +Viewerbase.annotateTextUtils = annotateTextUtils; + +// textMarkerUtils.* +import { textMarkerUtils } from './lib/textMarkerUtils'; +Viewerbase.textMarkerUtils = textMarkerUtils; + // createStacks.* import { createStacks } from './lib/createStacks'; Viewerbase.createStacks = createStacks; diff --git a/Packages/ohif-viewerbase/client/lib/StackManager.js b/Packages/ohif-viewerbase/client/lib/StackManager.js index 8bddcf0c0..b2558fbda 100644 --- a/Packages/ohif-viewerbase/client/lib/StackManager.js +++ b/Packages/ohif-viewerbase/client/lib/StackManager.js @@ -17,41 +17,50 @@ const stackUpdatedCallbacks = []; * @return {Array} Array with image IDs */ function createAndAddStack(stackMap, study, displaySet) { - const numImages = displaySet.images.length; - const imageIds = []; - let imageId; + const numImages = displaySet.images.length; + const imageIds = []; + let imageId; - displaySet.images.forEach((image, imageIndex) => { - const metaData = { - instance: image, - series: displaySet, // TODO: Check this - study: study, - numImages: numImages, - imageIndex: imageIndex + 1 - }; + displaySet.images.forEach((image, imageIndex) => { + const metaData = { + instance: image, + series: displaySet, // TODO: Check this + study: study, + numImages: numImages, + imageIndex: imageIndex + 1 + }; - const numberOfFrames = image.numberOfFrames; - if (numberOfFrames > 1) { - OHIF.log.info('Multiframe image detected'); - for (let i = 0; i < numberOfFrames; i++) { - metaData.frame = i; - imageId = getImageId(image, i); - imageIds.push(imageId); - addMetaData(imageId, metaData); - } - } - else { - imageId = getImageId(image); - imageIds.push(imageId); - addMetaData(imageId, metaData); - } - }); + const numberOfFrames = image.numberOfFrames; + if (numberOfFrames > 1) { + OHIF.log.info('Multiframe image detected'); + for (let i = 0; i < numberOfFrames; i++) { + metaData.frame = i; + imageId = getImageId(image, i); + imageIds.push(imageId); + addMetaData(imageId, metaData); + } + } + else { + imageId = getImageId(image); + imageIds.push(imageId); + addMetaData(imageId, metaData); + } + }); - return imageIds; + const stack = { + displaySetInstanceUid: displaySet.displaySetInstanceUid, + imageIds: imageIds, + frameRate: displaySet.frameRate, + isClip: displaySet.isClip + }; + + stackMap[displaySet.displaySetInstanceUid] = stack; + + return imageIds; } configuration = { - createAndAddStack: createAndAddStack + createAndAddStack: createAndAddStack }; /** @@ -61,64 +70,64 @@ configuration = { * clearStacks and makeAndAddStack should not used outside of loadStudy and removeStudies. */ const StackManager = { - /** - * Removes all current stacks - */ - clearStacks() { - stackMap = {}; - }, - /** - * Create a stack from an image set, as well as add in the metadata on a per image bases. - * @param study The study who's metadata will be added - * @param displaySet The set of images to make the stack from - * @return {Array} Array with image IDs - */ - makeAndAddStack(study, displaySet) { - return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks); - }, - /** - * Find a stack from the currently created stacks. - * @param displaySetInstanceUid The UID of the stack to find. - * @returns {*} undefined if not found, otherwise the stack object is returned. - */ - findStack(displaySetInstanceUid) { - return stackMap[displaySetInstanceUid]; - }, - /** - * Gets the underlying map of displaySetInstanceUid to stack object. - * WARNING: Do not change this object. It directly affects the manager. - * @returns {{}} map of displaySetInstanceUid -> stack. - */ - getAllStacks() { - return stackMap; - }, - /** - * Adds in a callback to be called on a stack being added / updated. - * @param callback must accept at minimum one argument, - * which is the stack that was added / updated. - */ - addStackUpdatedCallback(callback) { - if (typeof callback !== 'function') { - throw new OHIFError('callback must be provided as a function'); + /** + * Removes all current stacks + */ + clearStacks() { + stackMap = {}; + }, + /** + * Create a stack from an image set, as well as add in the metadata on a per image bases. + * @param study The study who's metadata will be added + * @param displaySet The set of images to make the stack from + * @return {Array} Array with image IDs + */ + makeAndAddStack(study, displaySet) { + return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks); + }, + /** + * Find a stack from the currently created stacks. + * @param displaySetInstanceUid The UID of the stack to find. + * @returns {*} undefined if not found, otherwise the stack object is returned. + */ + findStack(displaySetInstanceUid) { + return stackMap[displaySetInstanceUid]; + }, + /** + * Gets the underlying map of displaySetInstanceUid to stack object. + * WARNING: Do not change this object. It directly affects the manager. + * @returns {{}} map of displaySetInstanceUid -> stack. + */ + getAllStacks() { + return stackMap; + }, + /** + * Adds in a callback to be called on a stack being added / updated. + * @param callback must accept at minimum one argument, + * which is the stack that was added / updated. + */ + addStackUpdatedCallback(callback) { + if (typeof callback !== 'function') { + throw new OHIFError('callback must be provided as a function'); + } + stackUpdatedCallbacks.push(callback); + }, + /** + * Return configuration + */ + getConfiguration() { + return configuration; + }, + /** + * Set configuration, in order to provide compatibility + * with other systems by overriding this functions + * @param {Object} config object with functions to be overrided + * + * For now, only makeAndAddStack can be overrided + */ + setConfiguration(config) { + configuration = config; } - stackUpdatedCallbacks.push(callback); - }, - /** - * Return configuration - */ - getConfiguration() { - return configuration; - }, - /** - * Set configuration, in order to provide compatibility - * with other systems by overriding this functions - * @param {Object} config object with functions to be overrided - * - * For now, only makeAndAddStack can be overrided - */ - setConfiguration(config) { - configuration = config; - } }; export { StackManager }; diff --git a/Packages/ohif-viewerbase/client/lib/annotateTextUtils.js b/Packages/ohif-viewerbase/client/lib/annotateTextUtils.js new file mode 100644 index 000000000..b2f6014a3 --- /dev/null +++ b/Packages/ohif-viewerbase/client/lib/annotateTextUtils.js @@ -0,0 +1,122 @@ +import { viewportUtils } from './viewportUtils'; + +const getTextCallback = doneChangingTextCallback => { + // This handles the text entry for the annotation tool + const keyPressHandler = e => { + // If Enter or Esc are pressed, close the dialog + if (e.which === 13 || e.which === 27) { + closeHandler(); + } + }; + + const closeHandler = () => { + dialog.get(0).close(); + doneChangingTextCallback(getTextInput.val()); + // Reset the text value + getTextInput.val(''); + + // Reset the focus to the active viewport element + // This makes the mobile Safari keyboard close + const element = viewportUtils.getActiveViewportElement(); + $(element).focus(); + }; + + const dialog = $('#annotationDialog'); + if (dialog.get(0).open === true) { + return; + } + + const getTextInput = $('.annotationTextInput'); + + // Focus on the text input to open the Safari keyboard + getTextInput.focus(); + + dialog.get(0).showModal(); + + const confirm = dialog.find('.annotationDialogConfirm'); + confirm.off('click'); + confirm.on('click', () => { + closeHandler(); + }); + + // Use keydown since keypress doesn't handle ESC in Chrome + dialog.off('keydown'); + dialog.on('keydown', keyPressHandler); +}; + +const changeTextCallback = (data, eventData, doneChangingTextCallback) => { + const dialog = $('#relabelAnnotationDialog'); + if (dialog.get(0).open === true) { + return; + } + + if (Viewerbase.helpers.isTouchDevice()) { + // Center the dialog on screen on touch devices + dialog.css({ + top: 0, + left: 0, + right: 0, + bottom: 0, + margin: 'auto' + }); + } else { + // Place the dialog above the tool that is being relabelled + dialog.css({ + top: eventData.currentPoints.page.y - dialog.outerHeight() - 20, + left: eventData.currentPoints.page.x - dialog.outerWidth() / 2 + }); + } + + const getTextInput = dialog.find('.annotationTextInput'); + const confirm = dialog.find('.relabelConfirm'); + const remove = dialog.find('.relabelRemove'); + + getTextInput.val(data.text); + + // Focus on the text input to open the Safari keyboard + getTextInput.focus(); + + dialog.get(0).showModal(); + + confirm.off('click'); + confirm.on('click', () => { + dialog.get(0).close(); + doneChangingTextCallback(data, getTextInput.val()); + }); + + // If the remove button is clicked, delete this marker + remove.off('click'); + remove.on('click', () => { + dialog.get(0).close(); + doneChangingTextCallback(data, undefined, true); + }); + + dialog.off('keydown'); + dialog.on('keydown', keyPressHandler); + + const keyPressHandler = e => { + // If Enter is pressed, close the dialog + if (e.which === 13) { + closeHandler(); + } + }; + + const closeHandler = () => { + dialog.get(0).close(); + doneChangingTextCallback(data, getTextInput.val()); + // Reset the text value + getTextInput.val(''); + + // Reset the focus to the active viewport element + // This makes the mobile Safari keyboard close + const element = viewportUtils.getActiveViewportElement(); + $(element).focus(); + }; +}; + +const annotateTextUtils = { + getTextCallback, + changeTextCallback +}; + +export { annotateTextUtils }; \ No newline at end of file diff --git a/Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js b/Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js index 5cf6601f2..f0fc57be5 100644 --- a/Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js +++ b/Packages/ohif-viewerbase/client/lib/classes/LayoutManager.js @@ -88,13 +88,18 @@ export class LayoutManager { // Generate the additional data based on the appendix const additionalData = []; appendix.forEach((displaySet, index) => { - additionalData.push({ - viewportIndex: currentViewportIndex + index, - studyInstanceUid: displaySet.studyInstanceUid, - seriesInstanceUid: displaySet.seriesInstanceUid, - displaySetInstanceUid: displaySet.displaySetInstanceUid, - sopInstanceUid: displaySet.images[0].sopInstanceUid - }); + const { images, studyInstanceUid, seriesInstanceUid, displaySetInstanceUid } = displaySet; + const sopInstanceUid = images[0] && images[0].getSOPInstanceUID ? images[0].getSOPInstanceUID() : ''; + const viewportIndex = currentViewportIndex + index; + const data = { + viewportIndex, + studyInstanceUid, + seriesInstanceUid, + displaySetInstanceUid, + sopInstanceUid + }; + + additionalData.push(data); }); // Append the additional data with the viewport data diff --git a/Packages/ohif-viewerbase/client/lib/createStacks.js b/Packages/ohif-viewerbase/client/lib/createStacks.js index 50b4852e1..f13e9d78e 100644 --- a/Packages/ohif-viewerbase/client/lib/createStacks.js +++ b/Packages/ohif-viewerbase/client/lib/createStacks.js @@ -1,40 +1,86 @@ import { ImageSet } from './classes/ImageSet'; import { isImage } from './isImage'; +const isMultiFrame = instance => { + return instance.getRawValue('NumberOfFrames') > 1; +}; + +const makeDisplaySet = (series, instances) => { + const instance = instances[0]; + + const imageSet = new ImageSet(instances); + const seriesData = series.getData(); + + // set appropriate attributes to image set... + imageSet.setAttributes({ + displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID + seriesInstanceUid: seriesData.seriesInstanceUid, + seriesNumber: seriesData.seriesNumber, + seriesDescription: seriesData.seriesDescription, + numImageFrames: instances.length, + frameRate: instance.getRawValue('x00181063'), + modality: seriesData.modality, + isMultiFrame: isMultiFrame(instance) + }); + + // Sort the images in this series + imageSet.sortBy((a, b) => { + // Sort by instance Number + const aInstanceNumber = a.getRawValue('x00200013'); + const bInstanceNumber = b.getRawValue('x00200013'); + if (a.instanceNumber && b.instanceNumber && + a.instanceNumber !== b.instanceNumber) { + return a.instanceNumber - b.instanceNumber; + } + }); + + // Include the first image instance number (after sorted) + imageSet.setAttribute('instanceNumber', imageSet.getImage(0).getRawValue('x00200013')); + + return imageSet; +}; + +const isSingleImageModality = modality => { + return (modality === 'CR' || + modality === 'MG' || + modality === 'DX'); +}; + /** - * Creates a set of series to be placed in the Study Browser - * The series that appear in the Study Browser must represent + * Creates a set of series to be placed in the Study Metadata + * The series that appear in the Study Metadata must represent * imaging modalities. * * Furthermore, for drag/drop functionality, * it is easiest if the stack objects also contain information about * which study they are linked to. * - * @param study The study instance to be used - * @returns {Array} An array of series to be placed in the Study Browser + * @param study The study instance metadata to be used + * @returns {Array} An array of series to be placed in the Study Metadata */ -function createStacks(study) { +const createStacks = study => { // Define an empty array of display sets - var displaySets = []; + const displaySets = []; - if (!study || !study.seriesList) { - return displaySets; + if (!study || !study.getSeriesCount()) { + return displaySets; } - study.seriesList.forEach(series => { + // Loop through the series (SeriesMetadata) + study.forEachSeries(series => { // If the series has no instances, skip it - if (!series.instances) { + if (!series.getInstanceCount()) { return; } - // Search through the instances of this series + // Search through the instances (InstanceMedatada object) of this series // Split Multi-frame instances and Single-image modalities // into their own specific display sets. Place the rest of each // series into another display set. - let stackableInstances = []; - series.instances.forEach(instance => { - // All imaging modalities must have a valid value for sopClassUid or rows - if (!isImage(instance.sopClassUid) && !instance.rows) { + const stackableInstances = []; + series.forEachInstance(instance => { + // All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010) + if (!isImage(instance.getRawValue('x00080016')) && !instance.getRawValue('x00280010')) { return; } @@ -63,8 +109,8 @@ function createStacks(study) { }); if (stackableInstances.length) { - let displaySet = makeDisplaySet(series, stackableInstances); - displaySet.setAttribute('studyInstanceUid', study.studyInstanceUid); + const displaySet = makeDisplaySet(series, stackableInstances); + displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID()); displaySets.push(displaySet); } }); @@ -72,47 +118,6 @@ function createStacks(study) { return displaySets; }; -function makeDisplaySet(series, instances) { - const instance = instances[0]; - - const imageSet = new ImageSet(instances); - - // set appropriate attributes to image set... - imageSet.setAttributes({ - displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID - seriesInstanceUid: series.seriesInstanceUid, - seriesNumber: series.seriesNumber, - seriesDescription: series.seriesDescription, - numImageFrames: instances.length, - frameRate: instance.frameTime, - modality: series.modality, - isMultiFrame: isMultiFrame(instance) - }); - - // Sort the images in this series - imageSet.sortBy(function(a, b) { - if (a.instanceNumber && b.instanceNumber && - a.instanceNumber !== b.instanceNumber) { - return a.instanceNumber - b.instanceNumber; - } - }); - - // Include the first image instance number (after sorted) - imageSet.setAttribute('instanceNumber', imageSet.getImage(0).instanceNumber); - - return imageSet; -} - -function isSingleImageModality(modality) { - return (modality === 'CR' || - modality === 'MG' || - modality === 'DX'); -} - -function isMultiFrame(instance) { - return instance.numberOfFrames > 1; -} - /** * Expose "createStacks"... */ diff --git a/Packages/ohif-viewerbase/client/lib/getImageId.js b/Packages/ohif-viewerbase/client/lib/getImageId.js index c9526ff4b..d7d70b9fb 100644 --- a/Packages/ohif-viewerbase/client/lib/getImageId.js +++ b/Packages/ohif-viewerbase/client/lib/getImageId.js @@ -7,7 +7,13 @@ import { getWADORSImageId } from './getWADORSImageId'; * @param instance * @returns {string} The imageId to be used by Cornerstone */ -export function getImageId(instance, frame) { +export function getImageId(instanceMetadata, frame) { + if (!instanceMetadata) { + return; + } + + const instance = instanceMetadata.getData(); + if (!instance) { return; } diff --git a/Packages/ohif-viewerbase/client/lib/getWADORSImageId.js b/Packages/ohif-viewerbase/client/lib/getWADORSImageId.js index e52146394..bd79d101b 100644 --- a/Packages/ohif-viewerbase/client/lib/getWADORSImageId.js +++ b/Packages/ohif-viewerbase/client/lib/getWADORSImageId.js @@ -4,28 +4,30 @@ import { OHIF } from 'meteor/ohif:core'; /** * Obtain an imageId for Cornerstone based on the WADO-RS scheme * - * @param instance + * @param {object} instance metadata object (InstanceMetadata) * @returns {string} The imageId to be used by Cornerstone */ -export function getWADORSImageId(instance) { - var columnPixelSpacing = 1.0; - var rowPixelSpacing = 1.0; +export function getWADORSImageId(instanceMetada) { + const instance = instanceMetada.getData(); + let columnPixelSpacing = 1.0; + let rowPixelSpacing = 1.0; + if (instance.pixelSpacing) { - var split = instance.pixelSpacing.split('\\'); + const split = instance.pixelSpacing.split('\\'); rowPixelSpacing = parseFloat(split[0]); columnPixelSpacing = parseFloat(split[1]); } - var windowWidth; - var windowCenter; + let windowWidth; + let windowCenter; if (instance.windowWidth && instance.windowCenter) { windowWidth = parseFloat(instance.windowWidth.split('\\')[0]); windowCenter = parseFloat(instance.windowCenter.split('\\')[0]); } - var image = { + const image = { uri: Meteor.absoluteUrl(instance.wadorsuri), //imageId : '', //minPixelValue : 0, @@ -56,7 +58,7 @@ export function getWADORSImageId(instance) { instance: instance }; - var imageId = cornerstoneWADOImageLoader.imageManager.add(image); + const imageId = cornerstoneWADOImageLoader.imageManager.add(image); OHIF.log.info('WADO-RS ImageID: ' + imageId); return imageId; diff --git a/Packages/ohif-viewerbase/client/lib/sortingManager.js b/Packages/ohif-viewerbase/client/lib/sortingManager.js index 355e4507e..801d041fb 100644 --- a/Packages/ohif-viewerbase/client/lib/sortingManager.js +++ b/Packages/ohif-viewerbase/client/lib/sortingManager.js @@ -1,7 +1,7 @@ -import { getSortedData } from './getSortedData'; +import { createStacks } from './createStacks'; const getDisplaySets = (studyMetadata, seriesNumber, iteratorFunction) => { - const iteratorFn = typeof iteratorFunction !== 'function' ? getSortedData : iteratorFunction; + const iteratorFn = typeof iteratorFunction !== 'function' ? createStacks : iteratorFunction; return iteratorFn(studyMetadata, seriesNumber); }; diff --git a/Packages/ohif-viewerbase/client/lib/textMarkerUtils.js b/Packages/ohif-viewerbase/client/lib/textMarkerUtils.js new file mode 100644 index 000000000..6efc29e8a --- /dev/null +++ b/Packages/ohif-viewerbase/client/lib/textMarkerUtils.js @@ -0,0 +1,87 @@ +import { Blaze } from 'meteor/blaze'; + +import { toolManager } from './toolManager'; +import { viewportUtils } from './viewportUtils'; + +const changeTextCallback = (data, eventData, doneChangingTextCallback) => { + // This handles the double-click/long-press event on Spine text marker labels + const keyPressHandler = e => { + // If Enter or Esc are pressed, close the dialog + if (e.which === 13 || e.which === 27) { + closeHandler(); + } + }; + + // Deactivate textMarker tool after editing a spine label & if spine is not active tool + const deactivateAfterEdit = () => { + if (toolManager.getActiveTool() !== 'spine') { + const element = viewportUtils.getActiveViewportElement(); + cornerstoneTools.textMarker.deactivate(element, 1); + } + }; + + const closeHandler = () => { + dialog.get(0).close(); + doneChangingTextCallback(data, select.val()); + deactivateAfterEdit(); + // Reset the focus to the active viewport element + // This makes the mobile Safari keyboard close + const element = viewportUtils.getActiveViewportElement(); + $(element).focus(); + }; + + const dialog = $('#textMarkerRelabelDialog'); + + // Is necessary to use Blaze object to not create + // circular depencency with helper object (./helpers) + if (Blaze._globalHelpers.isTouchDevice()) { + // Center the dialog on screen on touch devices + dialog.css({ + top: 0, + left: 0, + right: 0, + bottom: 0, + margin: 'auto' + }); + dialog.find('.dialog.arrow').hide(); + } else { + // Place the dialog above the tool that is being relabelled + // TODO = Switch this to the tool coordinates, but put back into + // page coordinates. + dialog.css({ + top: eventData.currentPoints.page.y - dialog.outerHeight() - 20, + left: eventData.currentPoints.page.x - dialog.outerWidth() / 2 + }); + dialog.find('.dialog.arrow').show(); + } + + const select = dialog.find('.relabelSelect'); + const confirm = dialog.find('.relabelConfirm'); + const remove = dialog.find('.relabelRemove'); + + // If the remove button is clicked, delete this marker + remove.off('click'); + remove.on('click', () => { + dialog.get(0).close(); + doneChangingTextCallback(data, undefined, true); + deactivateAfterEdit(); + }); + + dialog.get(0).showModal(); + $('.relabelSelect').val(data.text).trigger('change'); //Update selector to the current + + confirm.off('click'); + confirm.on('click', () => { + closeHandler(); + }); + + // Use keydown since keypress doesn't handle ESC in Chrome + dialog.off('keydown'); + dialog.on('keydown', keyPressHandler); +}; + +const textMarkerUtils = { + changeTextCallback +}; + +export { textMarkerUtils }; \ No newline at end of file diff --git a/Packages/ohif-viewerbase/client/lib/toolManager.js b/Packages/ohif-viewerbase/client/lib/toolManager.js index b9d19587c..57c971aa9 100644 --- a/Packages/ohif-viewerbase/client/lib/toolManager.js +++ b/Packages/ohif-viewerbase/client/lib/toolManager.js @@ -4,6 +4,8 @@ import { OHIF } from 'meteor/ohif:core'; import { getFrameOfReferenceUID } from './getFrameOfReferenceUID'; import { updateCrosshairsSynchronizer } from './updateCrosshairsSynchronizer'; import { crosshairsSynchronizers } from './crosshairsSynchronizers'; +import { annotateTextUtils } from './annotateTextUtils'; +import { textMarkerUtils } from './textMarkerUtils'; let activeTool = 'wwwc'; let defaultTool = 'wwwc'; @@ -167,16 +169,34 @@ export const toolManager = { ...shadowConfig }); - // Set the configuration values for the text annotation (Arrow) tool + // Set the configuration values for the Text Marker (Spine Labelling) tool + const startFrom = $('#startFrom'); + const ascending = $('#ascending'); + const textMarkerConfig = { + markers: [ 'L5', 'L4', 'L3', 'L2', 'L1', // Lumbar spine + 'T12', 'T11', 'T10', 'T9', 'T8', 'T7', // Thoracic spine + 'T6', 'T5', 'T4', 'T3', 'T2', 'T1', + 'C7', 'C6', 'C5', 'C4', 'C3', 'C2', 'C1', // Cervical spine + ], + current: startFrom.val(), + ascending: ascending.is(':checked'), + loop: true, + changeTextCallback: textMarkerUtils.changeTextCallback, + shadow: shadowConfig.shadow, + shadowColor: shadowConfig.shadowColor, + shadowOffsetX: shadowConfig.shadowOffsetX, + shadowOffsetY: shadowConfig.shadowOffsetY + }; + cornerstoneTools.textMarker.setConfiguration(textMarkerConfig); - // @TODO: Fix this, needs to import them from somewhere - /*const annotateConfig = { - getTextCallback: getAnnotationTextCallback, - changeTextCallback: changeAnnotationTextCallback, + // Set the configuration values for the text annotation (Arrow) tool + const annotateConfig = { + getTextCallback: annotateTextUtils.getTextCallback, + changeTextCallback: annotateTextUtils.changeTextCallback, drawHandles: false, arrowFirst: true }; - arrowAnnotate.setConfiguration(annotateConfig);*/ + arrowAnnotate.setConfiguration(annotateConfig); const zoomConfig = { minScale: 0.05,